Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,12 @@ This chapter describes all the configuration parameters and their default values
|---------|-----------|---------------|-------------|
| `oiosaml.servlet.entityid` | Yes | | The EntityID which identifies the application as a Service Provider, e.g. `http://saml.serviceprovider.com`. |
| `oiosaml.servlet.baseurl` | Yes | | The URL on which the application is accessible in a web-browser. The value is used to generate SAML metadata, which must contain login/logout URL endpoints, e.g. `https://serviceprovider.com`. The value above results in generated metadata URLs like `https://serviceprovider.com/saml/assertionConsumer`. |
| `oiosaml.servlet.keystore.location` | Yes | | The name of the PKCS#12 keystore file, located on the classpath of the application. |
| `oiosaml.servlet.keystore.location` | Yes | | A reference to the PKCS#12 keystore file. Resolved from the classpath first, falling back to a filesystem path (absolute, or relative to the working directory), so the keystore can be deployed with the application or held outside it. |
| `oiosaml.servlet.keystore.password` | Yes | | The password to the PKCS#12 keystore given above. |
| `oiosaml.servlet.keystore.alias` | Yes | | The alias of the key entry in the PKCS#12 keystore given above. |
| `oiosaml.servlet.idp.entityid` | Yes | | The EntityID of the SAML Identity Provider that is used for login. |
| `oiosaml.servlet.idp.metadata.file` | Partially | | A FILE reference to the SAML Identity Provider metadata. The file must be located on the classpath of the application. Note that either a FILE or URL reference is required. |
| `oiosaml.servlet.idp.metadata.url` | Partially | | A URL reference to the SAML Identity Provider metadata. Note that either a FILE or URL reference is required. |
| `oiosaml.servlet.configurationfile` | No | | A FILE reference to a configuration file. If supplied, the `DispatcherServlet` will read its configuration from that file instead of the `ServletConfig` section. See [DispatcherServlet configuration from file](#dispatcherservlet-configuration-from-file). |
| `oiosaml.servlet.idp.metadata.file` | Yes | | A reference to the SAML Identity Provider metadata file. Resolved from the classpath first, falling back to a filesystem path (absolute, or relative to the working directory), so the metadata can be deployed with the application or held outside it. Download the metadata from the Identity Provider and deploy it with the application: NemLog-in does not sign its metadata, so trust in it comes from deploying the file, not from the transport it was fetched over. Note that metadata packed inside a war/jar is read once at startup and copied to a temporary file; for the periodic refresh to pick up changes, the value must resolve to a real filesystem path. |
| `oiosaml.servlet.configurationfile` | No | | A reference to a configuration file. Resolved from the classpath first, falling back to a filesystem path (absolute, or relative to the working directory), so the configuration can be deployed with the application or held outside it. If supplied, the properties in the file are merged on top of the `init-param`s of the `ServletConfig` section, overriding any key given in both places. If the file cannot be read, a warning is logged and the `init-param`s are used unchanged. See [DispatcherServlet configuration from file](#dispatcherservlet-configuration-from-file). |
| `oiosaml.servlet.profile.validation.enabled` | No | `true` | By default, the framework performs OIO SAML 3.0 profile validation. If this is not needed, turn off this setting by setting the value to `false`. |
| `oiosaml.servlet.profile.validation.assurancelevel.allowed` | No | `false` | The NemLog-in IdP cannot for all authentication provide a NSIS LoA. Therefore the service provider can decide to accept the AssuranceLevel which the NemLog-in IdP provides instead. If this is not acceptable, turn off this setting by omitting it or setting the value to `false`. |
| `oiosaml.servlet.profile.validation.assurancelevel.minimum` | No | `3` | If the AssuranceLevel is acceptable, a minimum value can be specified using this setting. Any integer is accepted, however the NemLog-in IdP will never provide an integer larger than 3. |
Expand All @@ -180,7 +179,6 @@ This chapter describes all the configuration parameters and their default values
| `oiosaml.servlet.secondary.page.error` | No | | The framework has a built-in error page, which is shown in case of SAML related errors. Set this value to redirect the user to another webpage in case of errors. See [Additional configuration](#additional-configuration) for details on getting error information. |
| `oiosaml.servlet.secondary.page.logout` | No | | The framework redirects the user to the context root of the web application after a successful logout. Set this value to redirect the user to another webpage instead of the context root. |
| `oiosaml.servlet.secondary.page.login` | No | | When the login process completes, the framework attempts to redirect the user to the web-resource they tried to access before login started. If this fails, the framework redirects to the page specified by the value. If no value is specified, the context root of the application is used. |
| `oiosaml.servlet.trust.selfsigned.certs` | No | `false` | By default, the framework performs certificate validation when accessing HTTPS-protected resources like SAML metadata. Set this value to `true` to disable certificate validation. |
| `oiosaml.servlet.revocation.crl.check.enabled` | No | `true` | By default, the framework performs revocation checking using OCSP and CRL. Set this value to `false` to disable CRL revocation checking. |
| `oiosaml.servlet.revocation.ocsp.check.enabled` | No | `true` | By default, the framework performs revocation checking using OCSP and CRL. Set this value to `false` to disable OCSP revocation checking. |
| `oiosaml.servlet.routing.path.prefix` | No | `saml` | Routing configuration: servlet path prefix for the OIO dispatch servlet. Change this to change where the OIOSAML 3 endpoint is mounted in the application context (`oiosaml.servlet.baseurl`). Example: with the default, the logout action is hit at `/{prefix}/{suffix.logout}` = `/saml/logout`. |
Expand Down Expand Up @@ -213,11 +211,11 @@ This chapter describes all the configuration parameters and their default values

### DispatcherServlet configuration from file

If the `oiosaml.servlet.configurationfile` setting is supplied to the `DispatcherServlet`, it will read its configuration from the supplied file instead. This file should be an ordinary property file, supplying properties as key/value pairs like the example below:
If the `oiosaml.servlet.configurationfile` setting is supplied to the `DispatcherServlet`, the properties in that file are merged on top of the `init-param`s, so a key given in both places takes its value from the file. The file is looked up on the classpath first, then as a filesystem path. It should be an ordinary property file, supplying properties as key/value pairs like the example below:

```properties
oiosaml.servlet.idp.entityid=https://saml.test-nemlog-in.dk/
oiosaml.servlet.idp.metadata.url=https://test-nemlog-in.dk/Testportal/Test-nemlog-in-2.xml
oiosaml.servlet.idp.metadata.file=test-nemlog-in-idp-metadata.xml
```

### Multiple AuthenticatedFilters and step-up
Expand Down
8 changes: 4 additions & 4 deletions demo/src/main/resources/oiosaml.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ oiosaml.servlet.keystore.alias=java.referenceimplementering
# for use with NemLog-in2
oiosaml.servlet.entityid=https://saml.oiosaml3-demo-app
oiosaml.servlet.baseurl=https://localhost:8443/oiosaml3-demo.java
# IntTest Idp
# IntTest Idp, metadata downloaded from the NemLog-in test portal and placed on the classpath
#oiosaml.servlet.idp.entityid=https://saml.test-nemlog-in.dk/
#oiosaml.servlet.idp.metadata.url=https://test-nemlog-in.dk/Testportal/Test-nemlog-in-2.xml
#oiosaml.servlet.idp.metadata.file=test-nemlog-in-idp-metadata.xml
# DevTest4 Idp
oiosaml.servlet.idp.entityid=https://saml.test-devtest4-nemlog-in.dk
oiosaml.servlet.idp.metadata.file=test-devtest4-idp-metadata.xml
Expand All @@ -16,8 +16,8 @@ oiosaml.servlet.idp.metadata.file=test-devtest4-idp-metadata.xml
#oiosaml.servlet.entityid=https://saml.oiosaml3-demo-app
#oiosaml.servlet.baseurl=https://localhost:8443/oiosaml3-demo.java
#oiosaml.servlet.idp.entityid=https://localhost:7080
#oiosaml.servlet.idp.metadata.url=https://localhost:7080/saml/metadata
#oiosaml.servlet.trust.selfsigned.certs=true
# Fetch https://localhost:7080/saml/metadata once and save it next to this file
#oiosaml.servlet.idp.metadata.file=test-idp-metadata.xml
oiosaml.servlet.revocation.crl.check.enabled=false
oiosaml.servlet.revocation.ocsp.check.enabled=false

Expand Down
29 changes: 2 additions & 27 deletions oiosaml/src/main/java/dk/gov/oio/saml/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public class Configuration {

// Metadata configuration
private String idpEntityID; // This IdP's EntityID
private String idpMetadataUrl; // The URL for the IdP Metadata
private String idpMetadataFile; // The file path for a metadata file
private int idpMetadataMinRefreshDelay = 1; // The minimum refresh delay in hours
private int idpMetadataMaxRefreshDelay = 12; // The maximum refresh delay in hours
Expand All @@ -57,7 +56,6 @@ public class Configuration {
private String logoutPage;
private String loginPage;
private String nameIDFormat = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
private boolean supportSelfSigned = false;

// Revocation check settings
private boolean crlCheckEnabled = true;
Expand Down Expand Up @@ -120,14 +118,6 @@ public void setIdpEntityID(String idpEntityID) {
this.idpEntityID = idpEntityID;
}

public String getIdpMetadataUrl() {
return idpMetadataUrl;
}

public void setIdpMetadataUrl(String idpMetadataUrl) {
this.idpMetadataUrl = idpMetadataUrl;
}

public String getIdpMetadataFile() {
return idpMetadataFile;
}
Expand Down Expand Up @@ -208,14 +198,6 @@ public void setSignatureAlgorithm(String signatureAlgorithm) {
this.signatureAlgorithm = signatureAlgorithm;
}

public boolean isSupportSelfSigned() {
return supportSelfSigned;
}

public void setSupportSelfSigned(boolean supportSelfSigned) {
this.supportSelfSigned = supportSelfSigned;
}

public int getClockSkew() {
return clockSkew;
}
Expand Down Expand Up @@ -453,7 +435,6 @@ public static class Builder {
private String spEntityID;
private String baseUrl;
private String idpEntityID;
private String idpMetadataUrl;
private String idpMetadataFile;
private String keystoreLocation;
private String keystorePassword;
Expand Down Expand Up @@ -489,8 +470,8 @@ public Configuration build() throws InternalException {
throw new InternalException("Cannot create configuration without IdP's entityID");
}

if (StringUtil.isEmpty(idpMetadataUrl) && StringUtil.isEmpty(idpMetadataFile)) {
throw new InternalException("Cannot create configuration without IdP Metadata URL or File location");
if (StringUtil.isEmpty(idpMetadataFile)) {
throw new InternalException("Cannot create configuration without the location of the IdP metadata file");
}

if (StringUtil.isEmpty(keystoreLocation)) {
Expand All @@ -510,7 +491,6 @@ public Configuration build() throws InternalException {
configuration.spEntityID = this.spEntityID;
configuration.baseUrl = this.baseUrl;
configuration.idpEntityID = this.idpEntityID;
configuration.idpMetadataUrl = this.idpMetadataUrl;
configuration.idpMetadataFile = this.idpMetadataFile;
configuration.keystoreLocation = this.keystoreLocation;
configuration.keystorePassword = this.keystorePassword;
Expand Down Expand Up @@ -551,11 +531,6 @@ public Builder setIdpEntityID(String idpEntityID) {
return this;
}

public Builder setIdpMetadataUrl(String idpMetadataUrl) {
this.idpMetadataUrl = idpMetadataUrl;
return this;
}

public Builder setIdpMetadataFile(String idpMetadataFile) {
this.idpMetadataFile = idpMetadataFile;
return this;
Expand Down
70 changes: 24 additions & 46 deletions oiosaml/src/main/java/dk/gov/oio/saml/model/IdPMetadata.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package dk.gov.oio.saml.model;

import java.io.ByteArrayInputStream;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
Expand All @@ -12,15 +9,8 @@
import java.util.Objects;
import java.util.Set;

import javax.net.ssl.SSLContext;

import dk.gov.oio.saml.util.ResourceUtil;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.TrustStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.bouncycastle.util.encoders.Base64;
Expand All @@ -30,7 +20,6 @@
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.metadata.resolver.impl.AbstractReloadingMetadataResolver;
import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver;
import org.opensaml.saml.metadata.resolver.impl.HTTPMetadataResolver;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
Expand All @@ -56,11 +45,9 @@ public class IdPMetadata {
private AbstractReloadingMetadataResolver resolver;
private DateTime lastCRLCheck;
private String entityId;
private String metadataURL;

public IdPMetadata(String entityId, String metadataURL, String metadataFilePath) throws ExternalException, InternalException {
public IdPMetadata(String entityId, String metadataFilePath) throws ExternalException, InternalException {
this.entityId = entityId;
this.metadataURL = metadataURL;
this.metadataFilePath = metadataFilePath;
getEntityDescriptor(); // Fetch metadata first time
}
Expand Down Expand Up @@ -96,27 +83,26 @@ public IDPSSODescriptor getSSODescriptor() throws ExternalException, InternalExc
return getEntityDescriptor().getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
}

public X509Certificate getValidX509Certificate(UsageType usageType) throws InternalException, ExternalException {
/**
* All certificates the IdP publishes for the given usage that passed revocation checking.
*
* <p>An IdP publishes both the outgoing and the incoming certificate while it rotates a key, and either
* of them can be the one in use at any moment, so callers have to accept all of them rather than picking
* one.</p>
*/
public List<X509Certificate> getValidX509Certificates(UsageType usageType) throws InternalException, ExternalException {
doRevocationCheck();

X509Certificate result = null;
List<X509Certificate> result = new ArrayList<>();
if (UsageType.ENCRYPTION.equals(usageType)) {
if (validEncryptionCertificates != null && !validEncryptionCertificates.isEmpty()) {
result = validEncryptionCertificates.get(0);
}
result.addAll(validEncryptionCertificates);
}
else if (UsageType.SIGNING.equals(usageType)) {
if (validSigningCertificates != null && !validSigningCertificates.isEmpty()) {
result = validSigningCertificates.get(0);
}
result.addAll(validSigningCertificates);
}

// If certificate is not found yet, try the unspecified
if (result == null) {
if (validUnspecifiedCertificates != null && !validUnspecifiedCertificates.isEmpty()) {
result = validUnspecifiedCertificates.get(0);
}
}
// Certificates published without a usage serve both purposes
result.addAll(validUnspecifiedCertificates);

return result;
}
Expand Down Expand Up @@ -245,28 +231,20 @@ private void initMetadataResolver() throws InternalException, ExternalException
try {
Configuration config = OIOSAML3Service.getConfig();

CloseableHttpClient httpClient;
if (config.isSupportSelfSigned()) {
TrustStrategy acceptingTrustStrategy = new TrustSelfSignedStrategy();
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
} else {
httpClient = HttpClients.createDefault();
}

if (metadataFilePath != null) {
log.debug("MetadataFilePath supplied. Using file based metadata resolver");
resolver = new FilesystemMetadataResolver(ResourceUtil.getResourceAsFile(metadataFilePath));
} else {
log.debug("MetadataFilePath not supplied. Using URL based metadata resolver");
resolver = new HTTPMetadataResolver(httpClient, metadataURL);
}
// Metadata is deployed as a file. The IdP does not sign its metadata, so trust in it comes
// from the deployment of that file, not from the transport it was fetched over
log.debug("Reading IdP metadata from {}", metadataFilePath);
resolver = new FilesystemMetadataResolver(ResourceUtil.getResourceAsFile(metadataFilePath));

resolver.setId(entityId);

// The file is re-read while the SP runs, so replacing it is enough to publish new keys
resolver.setMinRefreshDelay(1000L * 60 * 60 * config.getIdpMetadataMinRefreshDelay());
resolver.setMaxRefreshDelay(1000L * 60 * 60 * config.getIdpMetadataMaxRefreshDelay());
} catch (ResolverException | KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {

// Metadata that has passed its validUntil is not used
resolver.setRequireValidMetadata(true);
} catch (ResolverException e) {
throw new InternalException("Could not create MetadataResolver", e);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public IdPMetadata getIdPMetadata() throws ExternalException, InternalException
// This method is needed since we only have one IdP functionality for now.
Configuration config = OIOSAML3Service.getConfig();

return getIdPMetadata(config.getIdpEntityID(), config.getIdpMetadataUrl(), config.getIdpMetadataFile());
return getIdPMetadata(config.getIdpEntityID(), config.getIdpMetadataFile());
}

public SingleLogoutService getLogoutEndpoint() throws InternalException, ExternalException {
Expand All @@ -45,12 +45,12 @@ public String getLogoutResponseEndpoint() throws InternalException, ExternalExce
return getIdPMetadata().getLogoutResponseEndpoint();
}

private IdPMetadata getIdPMetadata(String idpEntityID, String idpMetadataURL, String idpMetadataFilePath) throws InternalException, ExternalException {
private IdPMetadata getIdPMetadata(String idpEntityID, String idpMetadataFilePath) throws InternalException, ExternalException {
IdPMetadata idPMetadata = identityProviders.get(idpEntityID);

// If IdP Metadata has not been fetched before, create object
if (idPMetadata == null) {
idPMetadata = new IdPMetadata(idpEntityID, idpMetadataURL, idpMetadataFilePath);
idPMetadata = new IdPMetadata(idpEntityID, idpMetadataFilePath);
identityProviders.put(idpEntityID, idPMetadata);
}

Expand Down
Loading
Loading