Skip to content

Commit

Permalink
[JENKINS-41907] obtain IdP Metadata from URL (#39)
Browse files Browse the repository at this point in the history
* feature to get the IdP Metadata from an URL

* fix tests

* AsyncAperiodicWork to update the IdP Metadata

* support get ipd metadata from URL

* run after start
process the XML from the URL and save it without the XML declaration

* add XML declaration to IdP metadata

* fix test
add base acceptan test

* exclusion on acceptance-test-harness to be able to package

* changes before merge

* merge with master

* finally the form works and do not have extrange side efects, I hate jelly

* form validation methods refactor

* validate that XML and URL are not blank

* fix test

* cleanup

* [JENKINS-47880] Navigating to /securityRealm/finishLogin manually shows an odd error

* [JENKINS-48030] SAML Azure AD exception

* [JENKINS-46063] do not allow blank passwords
  • Loading branch information
kuisathaverat committed Jan 9, 2018
1 parent 9bbb76e commit d3c1f8d
Show file tree
Hide file tree
Showing 20 changed files with 832 additions and 394 deletions.
38 changes: 38 additions & 0 deletions README.md
Expand Up @@ -39,6 +39,43 @@ When you face an issue you could try to enable a logger to these two packages on
* org.jenkinsci.plugins.saml - FINEST
* org.pac4j - FINE

**Azure AD**

After leaving the system for some period of time (like overnight) and trying to log in again you get this error

```
org.pac4j.saml.exceptions.SAMLException: No valid subject assertion found in response
at org.pac4j.saml.sso.impl.SAML2DefaultResponseValidator.validateSamlSSOResponse(SAML2DefaultResponseValidator.java:313)
at org.pac4j.saml.sso.impl.SAML2DefaultResponseValidator.validate(SAML2DefaultResponseValidator.java:138)
at org.pac4j.saml.sso.impl.SAML2WebSSOMessageReceiver.receiveMessage(SAML2WebSSOMessageReceiver.java:77)
at org.pac4j.saml.sso.impl.SAML2WebSSOProfileHandler.receive(SAML2WebSSOProfileHandler.java:35)
at org.pac4j.saml.client.SAML2Client.retrieveCredentials(SAML2Client.java:225)
at org.pac4j.saml.client.SAML2Client.retrieveCredentials(SAML2Client.java:60)
at org.pac4j.core.client.IndirectClient.getCredentials(IndirectClient.java:106)
at org.jenkinsci.plugins.saml.SamlProfileWrapper.process(SamlProfileWrapper.java:53)
at org.jenkinsci.plugins.saml.SamlProfileWrapper.process(SamlProfileWrapper.java:33)
at org.jenkinsci.plugins.saml.OpenSAMLWrapper.get(OpenSAMLWrapper.java:65)
at org.jenkinsci.plugins.saml.SamlSecurityRealm.doFinishLogin(SamlSecurityRealm.java:265)
at java.lang.invoke.MethodHandle.invokeWithArguments(MethodHandle.java:627)
at org.kohsuke.stapler.Function$MethodFunction.invoke(Function.java:343)
at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:184)
at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:117)
at org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:129)
at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:58)
at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:715)
Caused: javax.servlet.ServletException at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:765)
...
at winstone.BoundedExecutorService$1.run(BoundedExecutorService.java:77)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
```
* Click on logout button, then hit the Jenkins sign-in again.
* Clear the cookies in your browser or Go to Azure and sign out of the user name, then hit the Jenkins sign-in again.
* The max lifetime of the Access Token in Azure AD seems to be 24 hours where the refresh token can live for a maximum of 14 days (if the access token expires the refresh token is used to try to obtain a new access token). The Jenkins setting in Configure Global Security > SAML Identity Provider Settings > Maximum Authentication Lifetime is 24 hours (86400 in seconds) upping this to 1209600 (which is 14 days in seconds/the max lifetime of the Refresh Token).
* Enable the advanced "force authentication" setting is another workaround.


**Identity provider has no single sign on service available for the selected...**

Expand Down Expand Up @@ -67,6 +104,7 @@ org.pac4j.saml.exceptions.SAMLException: Identity provider has no single sign on

**Identity provider does not support encryption settings**

* Check the encryption methods, signing methods, and keys types supported by your IdP and set the encryption settings correctly
* Downgrade to 0.14 version, if it works, then enable encryption on that version to be sure that this is the issue
* Check the JDK version does not have issues like this [JDK-8176043](https://bugs.openjdk.java.net/browse/JDK-8176043)

Expand Down
14 changes: 14 additions & 0 deletions pom.xml
Expand Up @@ -104,6 +104,10 @@ under the License.
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
Expand Down Expand Up @@ -141,4 +145,14 @@ under the License.
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<configuration>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
247 changes: 247 additions & 0 deletions src/main/java/org/jenkinsci/plugins/saml/IdpMetadataConfiguration.java
@@ -0,0 +1,247 @@
package org.jenkinsci.plugins.saml;

import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import hudson.util.FormValidation;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import javax.annotation.Nonnull;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

import static org.jenkinsci.plugins.saml.SamlSecurityRealm.*;

/**
* Class to store the info about how to manage the IdP Metadata.
*/
public class IdpMetadataConfiguration extends AbstractDescribableImpl<IdpMetadataConfiguration> {
private static final Logger LOG = Logger.getLogger(IdpMetadataConfiguration.class.getName());

/**
* IdP Metadata on XML format, it implies there is not automatic updates.
*/
private String xml;

/**
* URL to update the IdP Metadata from.
*/
private String url;
/**
* Period in minutes between each IdP Metadata update.
*/
private Long period;

/**
* Jelly Constructor.
* @param xml Idp Metadata XML. if xml is null, url and period should not.
* @param url Url to download the IdP Metadata.
* @param period Period in minutes between updates of the IdP Metadata.
*/
@DataBoundConstructor
public IdpMetadataConfiguration(String xml, String url, Long period) {
this.xml = xml;
this.url = url;
this.period = period;
}

/**
* Inline Constructor.
* @param xml IdP Metadata XML.
*/
public IdpMetadataConfiguration(@Nonnull String xml) {
this.xml = xml;
this.period = 0L;
}

/**
* Idp Metadata downloaded from an Url Constructor.
* @param url URL to grab the IdP Metadata.
* @param period Period between updates of the IdP Metadata.
*/
public IdpMetadataConfiguration(@Nonnull String url, @Nonnull Long period) {
this.url = url;
this.period = period;
}

public String getXml() {
return xml;
}

public String getUrl() {
return url;
}

public Long getPeriod() {
return period;
}

/**
* @return Return the Idp Metadata from the XML file JENKINS_HOME/saml-idp.metadata.xml.
* @throws IOException in case it can not read the IdP Metadata file.
*/
public String getIdpMetadata() throws IOException {
return FileUtils.readFileToString(new File(SamlSecurityRealm.getIDPMetadataFilePath()));
}

/**
* Creates the IdP Metadata file (saml-idp.metadata.xml) in JENKINS_HOME using the configuration.
* @throws IOException in case of error writing the file.
*/
public void createIdPMetadataFile() throws IOException {
try {
if (StringUtils.isNotBlank(xml)) {
FileUtils.writeStringToFile(new File(SamlSecurityRealm.getIDPMetadataFilePath()), xml);
} else {
updateIdPMetadata();
}
} catch (IOException e) {
throw new IOException("Can not write IdP metadata file in JENKINS_HOME", e);
}
}

/**
* Gets the IdP Metadata from an URL, then validate it and write it to a file (JENKINS_HOME/saml-idp.metadata.xml).
* @throws IOException in case of error writing the file or validating the content.
*/
public void updateIdPMetadata() throws IOException {
try {
URLConnection urlConnection = new URL(url).openConnection();
try (InputStream in = urlConnection.getInputStream()) {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new StreamSource(in), new StreamResult(writer));
String xml = writer.toString();
FormValidation validation = new SamlValidateIdPMetadata(xml).get();
if (FormValidation.Kind.OK == validation.kind) {
FileUtils.writeStringToFile(new File(SamlSecurityRealm.getIDPMetadataFilePath()), xml);
} else {
throw new IllegalArgumentException(validation.getMessage());
}
}
} catch (IOException | TransformerException e) {
throw new IOException("Was not possible to update the IdP Metadata from the URL " + url, e);
}
}

/**
* {@inheritDoc}
*/
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("IdpMetadataConfiguration{");
sb.append("xml='").append(xml).append('\'');
sb.append(", url='").append(url).append('\'');
sb.append(", period=").append(period);
sb.append('}');
return sb.toString();
}

@Extension
public static final class DescriptorImpl extends Descriptor<IdpMetadataConfiguration> {
public DescriptorImpl() {
super();
}

public DescriptorImpl(Class<? extends IdpMetadataConfiguration> clazz) {
super(clazz);
}

@Override
public String getDisplayName() {
return "";
}

public FormValidation doTestIdpMetadata(@QueryParameter("xml") String xml) {
if (StringUtils.isBlank(xml)) {
return FormValidation.error(ERROR_IDP_METADATA_EMPTY);
}

return new SamlValidateIdPMetadata(xml).get();
}

public FormValidation doCheckPeriod(@QueryParameter("period") String period) {
if (StringUtils.isEmpty(period)) {
return FormValidation.error(ERROR_NOT_VALID_NUMBER);
}
long i = 0;
try {
i = Long.parseLong(period);
} catch (NumberFormatException e) {
return FormValidation.error(ERROR_NOT_VALID_NUMBER, e);
}

if (i < 0) {
return FormValidation.error(ERROR_NOT_VALID_NUMBER);
}

if (i > Integer.MAX_VALUE) {
return FormValidation.error(ERROR_NOT_VALID_NUMBER);
}

return FormValidation.ok();
}

public FormValidation doCheckXml(@QueryParameter("xml") String xml, @QueryParameter("url") String url) {
if (StringUtils.isBlank(xml) && StringUtils.isBlank(url)) {
return FormValidation.error(ERROR_IDP_METADATA_EMPTY);
}

return FormValidation.ok();
}

public FormValidation doCheckUrl(@QueryParameter("url") String url) {
if (StringUtils.isEmpty(url)) {
return FormValidation.ok();
}
try {
new URL(url);
} catch (MalformedURLException e) {
return FormValidation.error(ERROR_MALFORMED_URL, e);
}
return FormValidation.ok();
}

public FormValidation doTestIdpMetadataURL(@QueryParameter("url") String url) {
URLConnection urlConnection = null;
try {
urlConnection = new URL(url).openConnection();
} catch (IOException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
return FormValidation.error(NOT_POSSIBLE_TO_GET_THE_METADATA + url);
}

try (InputStream in = urlConnection.getInputStream()) {
String xml = IOUtils.toString(in,StringUtils.defaultIfEmpty(urlConnection.getContentEncoding(),"UTF-8"));
return new SamlValidateIdPMetadata(xml).get();
} catch (MalformedURLException e) {
return FormValidation.error(ERROR_MALFORMED_URL);
} catch (IOException e) {
LOG.log(Level.SEVERE, e.getMessage(), e);
return FormValidation.error(NOT_POSSIBLE_TO_GET_THE_METADATA + url);
}
}
}
}
3 changes: 0 additions & 3 deletions src/main/java/org/jenkinsci/plugins/saml/OpenSAMLWrapper.java
Expand Up @@ -93,9 +93,6 @@ protected WebContext createWebContext() {
* @return a SAML2Client object to interact with the IdP service.
*/
protected SAML2Client createSAML2Client() {
//FIXME [kuisathaverat][JENKINS-40144] Throws NPE I do not like it
Preconditions.checkNotNull(samlPluginConfig.getIdpMetadata());

final SAML2ClientConfiguration config = new SAML2ClientConfiguration();
config.setIdentityProviderMetadataResource(new SamlFileResource(SamlSecurityRealm.getIDPMetadataFilePath()));
config.setDestinationBindingType(samlPluginConfig.getBinding());
Expand Down

0 comments on commit d3c1f8d

Please sign in to comment.