getInitConfig() {
return configMap;
}
+ /**
+ * Do not start on if not supported configuration is provided:
+ *
+ * We no longer allow 'oiosaml.servlet.idp.metadata.url' to be specified - IdP metadata must be downloaded and deployed
+ * as it provides the only trust anchor.
+ *
+ * Configuration 'oiosaml.servlet.trust.selfsigned.certs' is also removed which was used for relaxing TLS trust
+ * when retrieving IdP metadata over TLS.
+ */
+ private void rejectRemovedConfiguration(Map config) throws ServletException {
+ String metadataUrl = config.get(Constants.REMOVED_IDP_METADATA_URL);
+ if (StringUtil.isNotEmpty(metadataUrl)) {
+ if (StringUtil.isNotEmpty(config.get(Constants.IDP_METADATA_FILE))) {
+ log.warn("'{}' is no longer supported and is ignored, IdP metadata is read from '{}'",
+ Constants.REMOVED_IDP_METADATA_URL, Constants.IDP_METADATA_FILE);
+ }
+ else {
+ throw new ServletException(String.format(
+ "'%s' is no longer supported. The IdP does not sign its metadata, so trust in it comes from deploying the metadata file. Download the metadata and point '%s' at it",
+ Constants.REMOVED_IDP_METADATA_URL, Constants.IDP_METADATA_FILE));
+ }
+ }
+
+ if ("true".equals(config.get(Constants.REMOVED_SUPPORT_SELF_SIGNED))) {
+ throw new ServletException(String.format(
+ "'%s' is no longer supported. It only relaxed TLS validation for fetching IdP metadata, which is now read from a file",
+ Constants.REMOVED_SUPPORT_SELF_SIGNED));
+ }
+ }
+
// Should make sure all handlers are initialized and added to the list
private void initServlet() throws ServletException {
if (!initialized) {
// convert to more useful map
Map config = getInitConfig();
+ rejectRemovedConfiguration(config);
+
try {
// create configuration with mandatory settings
@@ -283,7 +310,6 @@ private void initServlet() throws ServletException {
.setKeystorePassword(config.get(Constants.KEYSTORE_PASSWORD))
.setKeyAlias(config.get(Constants.KEY_ALIAS))
.setIdpEntityID(config.get(Constants.IDP_ENTITY_ID))
- .setIdpMetadataUrl(config.get(Constants.IDP_METADATA_URL))
.setIdpMetadataFile(config.get(Constants.IDP_METADATA_FILE))
.setServletRoutingPathPrefix(config.get(Constants.SP_ROUTING_BASE))
.setServletRoutingPathSuffixError(config.get(Constants.SP_ROUTING_ERROR))
diff --git a/oiosaml/src/main/java/dk/gov/oio/saml/util/Constants.java b/oiosaml/src/main/java/dk/gov/oio/saml/util/Constants.java
index 6ac31bb..37e5532 100644
--- a/oiosaml/src/main/java/dk/gov/oio/saml/util/Constants.java
+++ b/oiosaml/src/main/java/dk/gov/oio/saml/util/Constants.java
@@ -14,7 +14,9 @@ public class Constants {
public static final String KEY_ALIAS = "oiosaml.servlet.keystore.alias";
public static final String IDP_ENTITY_ID = "oiosaml.servlet.idp.entityid";
public static final String IDP_METADATA_FILE = "oiosaml.servlet.idp.metadata.file";
- public static final String IDP_METADATA_URL = "oiosaml.servlet.idp.metadata.url";
+ // Removed, IdP metadata is deployed as a file. Kept so a configuration still using it is rejected
+ // instead of silently ignored, see DispatcherServlet
+ public static final String REMOVED_IDP_METADATA_URL = "oiosaml.servlet.idp.metadata.url";
// Configuration constants for DispatcherServlet (optional, has default values)
public static final String EXTERNAL_CONFIGURATION_FILE = "oiosaml.servlet.configurationfile";
@@ -32,7 +34,7 @@ public class Constants {
public static final String ERROR_PAGE = "oiosaml.servlet.secondary.page.error";
public static final String LOGOUT_PAGE = "oiosaml.servlet.secondary.page.logout";
public static final String LOGIN_PAGE = "oiosaml.servlet.secondary.page.login";
- public static final String SUPPORT_SELF_SIGNED = "oiosaml.servlet.trust.selfsigned.certs";
+ public static final String REMOVED_SUPPORT_SELF_SIGNED = "oiosaml.servlet.trust.selfsigned.certs";
public static final String SP_ROUTING_BASE = "oiosaml.servlet.routing.path.prefix";
public static final String SP_ROUTING_ERROR = "oiosaml.servlet.routing.path.suffix.error";
public static final String SP_ROUTING_METADATA = "oiosaml.servlet.routing.path.suffix.metadata";
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/audit/AuditServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/audit/AuditServiceTest.java
index 07a42dd..5d11f9d 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/audit/AuditServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/audit/AuditServiceTest.java
@@ -49,7 +49,7 @@ void setupConfiguration() throws InternalException {
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setKeystoreLocation(TestConstants.SP_KEYSTORE_LOCATION)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
.setKeyAlias(TestConstants.SP_KEYSTORE_ALIAS)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/config/ConfigurationTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/config/ConfigurationTest.java
index 283a651..f070b8b 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/config/ConfigurationTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/config/ConfigurationTest.java
@@ -13,7 +13,7 @@ private Configuration minimalConfiguration() throws InternalException {
.setSpEntityID("https://sp.example.com")
.setBaseUrl("https://sp.example.com")
.setIdpEntityID("https://idp.example.com")
- .setIdpMetadataUrl("https://idp.example.com/metadata")
+ .setIdpMetadataFile("test-metadata.xml")
.setKeystoreLocation("keystore.p12")
.setKeystorePassword("password")
.setKeyAlias("alias")
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/filter/AuthenticatedFilterTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/filter/AuthenticatedFilterTest.java
index c8af77b..49171ba 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/filter/AuthenticatedFilterTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/filter/AuthenticatedFilterTest.java
@@ -57,7 +57,7 @@ public static void beforeAll(MockServerClient idp) throws Exception {
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation(TestConstants.SP_KEYSTORE_LOCATION)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/model/IdPMetadataTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/model/IdPMetadataTest.java
index 7f7be5b..389bfd3 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/model/IdPMetadataTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/model/IdPMetadataTest.java
@@ -29,7 +29,7 @@ public void testGetLogoutResponseEndpoint_WithEmptyResponseLocation() throws Ext
private void testSingleLogoutResponseLocation(String idpMetadataFileLocation, String expectedUri) throws ExternalException, InternalException {
ClassLoader classLoader = IdpMetadataServiceTest.class.getClassLoader();
String fileLocation = classLoader.getResource(idpMetadataFileLocation).getFile();
- IdPMetadata idpMetadata = new IdPMetadata("http://mockidp.localhost", null, fileLocation);
+ IdPMetadata idpMetadata = new IdPMetadata("http://mockidp.localhost", fileLocation);
String responseLocation = idpMetadata.getLogoutResponseEndpoint();
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/AuthnRequestServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/AuthnRequestServiceTest.java
index 951d43b..d443480 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/AuthnRequestServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/AuthnRequestServiceTest.java
@@ -50,7 +50,7 @@ public static void beforeAll(MockServerClient idp) throws Exception {
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation(TestConstants.SP_KEYSTORE_LOCATION)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/BaseServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/BaseServiceTest.java
index b9d5132..e6b457c 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/BaseServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/BaseServiceTest.java
@@ -29,7 +29,7 @@ public static void beforeAll(MockServerClient idp) throws Exception {
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation(keystoreLocation)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/CredentialServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/CredentialServiceTest.java
index d2d0a3a..1b0f0d2 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/CredentialServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/CredentialServiceTest.java
@@ -33,7 +33,7 @@ public void testKeystoreAliasIsCaseInsensitive() throws Exception {
.setSpEntityID(TestConstants.SP_ENTITY_ID)
.setBaseUrl(TestConstants.SP_BASE_URL)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setKeystoreLocation(keystoreLocation)
.setKeystorePassword("Test1234")
.setKeyAlias(ALIAS_IN_DIFFERENT_CASE)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/IdpMetadataServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/IdpMetadataServiceTest.java
index 5bc87a4..c9d5ca1 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/IdpMetadataServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/IdpMetadataServiceTest.java
@@ -52,38 +52,19 @@ public void testGetMetadataFromFile() throws Exception {
@DisplayName("Test retrieving metadata")
@Test
public void testGetMetadata() throws Exception {
- // Make sure Mock idp is setup to return correct data
- idp
- .when(
- request()
- .withMethod("GET")
- .withPath("/saml/metadata"),
- Times.exactly(1)
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(TestConstants.IDP_METADATA));
-
+ Configuration config = OIOSAML3Service.getConfig();
+ config.setIdpMetadataFile(TestConstants.idpMetadataFile());
+
EntityDescriptor entityDescriptor = IdPMetadataService.getInstance().getIdPMetadata().getEntityDescriptor();
Assertions.assertNotNull(entityDescriptor);
Assertions.assertEquals(TestConstants.IDP_ENTITY_ID, entityDescriptor.getEntityID());
}
-
+
@DisplayName("Test retrieving incorrect metadata")
@Test
public void testGetIncorrectMetadata() throws Exception {
- // Make sure Mock idp is setup to return incorrect data
- idp.when(
- request()
- .withMethod("GET")
- .withPath("/saml/metadata"),
- Times.exactly(1)
- )
- .respond(
- response()
- .withStatusCode(200)
- .withBody(TestConstants.BAD_IDP_METADATA));
+ Configuration config = OIOSAML3Service.getConfig();
+ config.setIdpMetadataFile(TestConstants.writeIdpMetadataFile(TestConstants.BAD_IDP_METADATA));
// we should get NULL back, if the EntityId does not match
EntityDescriptor entityDescriptor = IdPMetadataService.getInstance().getIdPMetadata().getEntityDescriptor();
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/OIOSAML3ServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/OIOSAML3ServiceTest.java
index 9ecada2..c6d3550 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/OIOSAML3ServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/OIOSAML3ServiceTest.java
@@ -24,7 +24,7 @@ void testInvalidKeystoreConfiguration() throws InternalException, Initialization
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation(TestConstants.SP_KEYSTORE_LOCATION)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
@@ -55,7 +55,7 @@ void testValidConfiguration() throws InternalException, InitializationException
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation(TestConstants.SP_KEYSTORE_LOCATION)
.setKeystorePassword(TestConstants.SP_KEYSTORE_PASSWORD)
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/service/validation/AssertionValidationServiceTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/service/validation/AssertionValidationServiceTest.java
index b8ad5e0..6bb7759 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/service/validation/AssertionValidationServiceTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/service/validation/AssertionValidationServiceTest.java
@@ -5,6 +5,7 @@
import dk.gov.oio.saml.service.AuthnRequestService;
import dk.gov.oio.saml.service.BaseServiceTest;
import dk.gov.oio.saml.service.IdPMetadataService;
+import dk.gov.oio.saml.service.OIOSAML3Service;
import dk.gov.oio.saml.session.AuthnRequestWrapper;
import dk.gov.oio.saml.util.ExternalException;
import dk.gov.oio.saml.util.IdpUtil;
@@ -123,6 +124,48 @@ public void testFailAssertionWithoutSpecVersion() throws Exception {
});
}
+ @DisplayName("Test that validator accepts a signature made with any of the signing certificates in metadata")
+ @Test
+ public void testValidateAssertionSignedWithSecondCertificateInMetadata() throws Exception {
+ AssertionValidationService validationService = new AssertionValidationService();
+
+ // Metadata where the certificate actually used for signing is preceded by another one, as it is
+ // while the IdP rotates its signing key
+ String otherCertificate = IdpUtil.getIdpCertificateBase64(false);
+ String metadata = TestConstants.IDP_METADATA.replaceFirst("",
+ ""
+ + otherCertificate + "");
+
+ String originalMetadataFile = OIOSAML3Service.getConfig().getIdpMetadataFile();
+ OIOSAML3Service.getConfig().setIdpMetadataFile(TestConstants.writeIdpMetadataFile(metadata));
+ IdPMetadataService.getInstance().clear(TestConstants.IDP_ENTITY_ID);
+
+ try {
+ // Mock HttpServletRequest
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ Mockito.when(request.getRequestURL()).thenReturn(new StringBuffer(TestConstants.SP_ASSERTION_CONSUMER_URL));
+
+ // Create AuthnRequest
+ AuthnRequestService authnRequestService = AuthnRequestService.getInstance();
+ AuthnRequest authnRequest = getAuthnRequest(authnRequestService);
+ String inResponseToId = authnRequest.getID();
+
+ // Create MessageContext, Response and Assertion
+ String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ MessageContext messageContext = IdpUtil.createMessageWithAssertion(true, true, true, nameID, TestConstants.SP_ENTITY_ID, TestConstants.SP_ASSERTION_CONSUMER_URL, inResponseToId);
+ Response response = (Response) messageContext.getMessage();
+
+ Assertion assertion = new AssertionService().getAssertion(response);
+
+ // Validate
+ validationService.validate(request, messageContext, response, assertion, new AuthnRequestWrapper(authnRequest, NSISLevel.SUBSTANTIAL, ""));
+ }
+ finally {
+ OIOSAML3Service.getConfig().setIdpMetadataFile(originalMetadataFile);
+ IdPMetadataService.getInstance().clear(TestConstants.IDP_ENTITY_ID);
+ }
+ }
+
@DisplayName("Test that validator will fail an assertion with the wrong destination")
@Test
public void testFailAssertionWithWrongDestination() throws Exception {
@@ -418,8 +461,8 @@ public void testFailSignatureNotBoundToAssertion() throws Exception {
// Only interesting while the signature itself still verifies, otherwise the test would pass for the
// wrong reason
- X509Certificate idpCertificate = IdPMetadataService.getInstance().getIdPMetadata().getValidX509Certificate(UsageType.SIGNING);
- SignatureValidator.validate(unboundAssertion.getSignature(), new BasicX509Credential(idpCertificate));
+ List idpCertificates = IdPMetadataService.getInstance().getIdPMetadata().getValidX509Certificates(UsageType.SIGNING);
+ SignatureValidator.validate(unboundAssertion.getSignature(), new BasicX509Credential(idpCertificates.get(0)));
// Validate, should fail because the signature is not bound to the assertion being consumed
AssertionValidationException exception = Assertions.assertThrows(AssertionValidationException.class, () -> {
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/servlet/LogoutRequestHandlerTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/servlet/LogoutRequestHandlerTest.java
index ccac42f..e178fc6 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/servlet/LogoutRequestHandlerTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/servlet/LogoutRequestHandlerTest.java
@@ -32,12 +32,15 @@
import org.w3c.dom.Element;
import dk.gov.oio.saml.model.NSISLevel;
+import dk.gov.oio.saml.service.IdPMetadataService;
import dk.gov.oio.saml.service.OIOSAML3Service;
import net.shibboleth.utilities.java.support.codec.Base64Support;
import net.shibboleth.utilities.java.support.xml.SerializeSupport;
public class LogoutRequestHandlerTest {
+ private static final String NAME_ID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+
@DisplayName("Test that a logged-in user can perform a logout")
@Test
public void testLogoutRequestWhenLoggedIn() throws InternalException, IOException, ExternalException, URISyntaxException {
@@ -50,7 +53,7 @@ public void testLogoutRequestWhenLoggedIn() throws InternalException, IOExceptio
// Mock session with state: not logged in at any NSIS level
Mockito.when(sessionHandler.isAuthenticated(session)).thenReturn(true);
Mockito.when(assertionWrapper.getNsisLevel()).thenReturn(NSISLevel.SUBSTANTIAL);
- Mockito.when(assertionWrapper.getSubjectNameId()).thenReturn("https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7");
+ Mockito.when(assertionWrapper.getSubjectNameId()).thenReturn(NAME_ID);
Mockito.when(assertionWrapper.getSubjectNameIdFormat()).thenReturn(NameID.PERSISTENT);
// Mock HttpServletRequest
@@ -66,7 +69,7 @@ public void testLogoutRequestWhenLoggedIn() throws InternalException, IOExceptio
LogoutRequestHandler logoutRequestHandler = new LogoutRequestHandler();
logoutRequestHandler.handleGet(request, response);
- Mockito.verify(sessionHandler).logout(session,assertionWrapper);
+ Mockito.verify(sessionHandler).logout(session, assertionWrapper);
Mockito.verify(session).invalidate();
Mockito.verify(response).sendRedirect(Mockito.anyString());
@@ -93,9 +96,9 @@ public void testIdPLogoutRequestWhenLoggedIn() throws Exception {
SessionHandler sessionHandler = OIOSAML3Service.getSessionHandlerFactory().getHandler();
// Create LogoutRequest
- String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ String nameID = NAME_ID;
MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(nameID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
- String sessionIndex = ((LogoutRequest)messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
+ String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
// Marshall and serialize
Element marshalledMessage = XMLObjectSupport.marshall(messageContext.getMessage());
@@ -143,7 +146,7 @@ public void testIdPLogoutRequestWhenLoggedIn() throws Exception {
logoutRequestHandler.handleGet(request, response);
// Verification
- Mockito.verify(sessionHandler).logout(session,assertionWrapper);
+ Mockito.verify(sessionHandler).logout(session, assertionWrapper);
Mockito.verify(session).invalidate();
Mockito.verify(outputStreamMock).flush(); //Verify that something is sent to the IdP
Mockito.verify(logoutRequestHandler).sendPost(Mockito.eq(response), contextArgumentCaptor.capture());
@@ -166,9 +169,9 @@ public void testIdPSOAPLogoutRequestWhenLoggedIn() throws Exception {
SessionHandler sessionHandler = OIOSAML3Service.getSessionHandlerFactory().getHandler();
// Create LogoutRequest
- String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ String nameID = NAME_ID;
MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(nameID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
- String sessionIndex = ((LogoutRequest)messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
+ String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
// Marshall and serialize
Element marshalledMessage = XMLObjectSupport.marshall(messageContext.getMessage());
@@ -193,7 +196,7 @@ public void testIdPSOAPLogoutRequestWhenLoggedIn() throws Exception {
Mockito.when(request.getMethod()).thenReturn("POST"); // Method: GET
Mockito.when(request.getContentType()).thenReturn("text/xml");
Mockito.when(request.getHeader("SOAPAction")).thenReturn("SOAPAction");
- Mockito.when(request.getInputStream()).thenReturn(new ServletInputStream(){
+ Mockito.when(request.getInputStream()).thenReturn(new ServletInputStream() {
public int read() throws IOException {
return inputStream.read();
}
@@ -231,7 +234,7 @@ public void setReadListener(ReadListener readListener) {
logoutRequestHandler.handleSOAP(request, response);
// Verification
- Mockito.verify(sessionHandler).logout(session,assertionWrapper);
+ Mockito.verify(sessionHandler).logout(session, assertionWrapper);
Mockito.verify(session).invalidate();
Mockito.verify(outputStreamMock).flush(); //Verify that something is sent to the IdP
Mockito.verify(logoutRequestHandler).sendSOAP(Mockito.eq(response), contextArgumentCaptor.capture());
@@ -245,7 +248,7 @@ public void setReadListener(ReadListener readListener) {
Assertions.assertEquals(TestConstants.SP_ENTITY_ID, logoutResponse.getIssuer().getValue());
Assertions.assertEquals(TestConstants.IDP_LOGOUT_RESPONSE_URL, logoutResponse.getDestination());
}
-
+
@DisplayName("Test that an IdP can request a logout with a signature on the query string")
@Test
public void testIdPLogoutRequestSignedOnQueryString() throws Exception {
@@ -255,7 +258,7 @@ public void testIdPLogoutRequestSignedOnQueryString() throws Exception {
// Create LogoutRequest without a signature on the message itself, the HTTP-Redirect binding signs
// the query string instead
- String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ String nameID = NAME_ID;
MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(nameID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, false, true);
String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
String redirectUrl = IdpUtil.encodeAsRedirectUrl(messageContext);
@@ -286,30 +289,57 @@ public void testIdPLogoutRequestSignedOnQueryString() throws Exception {
@Test
public void testRejectUnsignedLogoutRequest() throws Exception {
assertLogoutRequestRejected(IdpUtil.createMessageWithLogoutRequest(
- "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7",
- NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, false, true));
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, false, true));
}
@DisplayName("Test that a LogoutRequest signed with an unknown key is rejected")
@Test
public void testRejectLogoutRequestSignedWithUnknownKey() throws Exception {
assertLogoutRequestRejected(IdpUtil.createMessageWithLogoutRequest(
- "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7",
- NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, true, false));
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, true, false));
}
@DisplayName("Test that a LogoutRequest from another issuer is rejected")
@Test
public void testRejectLogoutRequestFromUnknownIssuer() throws Exception {
assertLogoutRequestRejected(IdpUtil.createMessageWithLogoutRequest(
- "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7",
- NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, true, true, "https://not-the-configured-idp"));
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, true, true, "https://not-the-configured-idp"));
+ }
+
+ @DisplayName("Test that a LogoutRequest signed with any of the signing certificates in metadata is accepted")
+ @Test
+ public void testLogoutRequestSignedWithSecondCertificateInMetadata() throws Exception {
+ MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
+
+ withIdPMetadata(metadataWithTwoSigningCertificates(), () -> assertLogoutRequestAccepted(messageContext, false));
+ }
+
+ @DisplayName("Test that a query string signed with any of the signing certificates in metadata is accepted")
+ @Test
+ public void testLogoutRequestSignedOnQueryStringWithSecondCertificateInMetadata() throws Exception {
+ MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL, false, true);
+
+ withIdPMetadata(metadataWithTwoSigningCertificates(), () -> assertLogoutRequestAccepted(messageContext, true));
+ }
+
+ @DisplayName("Test that a LogoutRequest is rejected when metadata holds no signing certificate")
+ @Test
+ public void testRejectLogoutRequestWhenMetadataHasNoSigningCertificate() throws Exception {
+ MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(
+ NAME_ID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
+
+ // Without a certificate there is nothing to validate the signature against, so the request cannot be
+ // accepted on the grounds that no validation failed
+ withIdPMetadata(metadataWithoutSigningCertificate(),
+ () -> assertLogoutRequestRejected(messageContext, InternalException.class));
}
/**
- * Send the LogoutRequest to the handler and require that it is refused without any session being touched.
+ * Send the LogoutRequest to the handler and require that the session it names is logged out.
*/
- private void assertLogoutRequestRejected(MessageContext messageContext) throws Exception {
+ private void assertLogoutRequestAccepted(MessageContext messageContext, boolean signedOnQueryString) throws Exception {
HttpSession session = Mockito.mock(HttpSession.class);
AssertionWrapper assertionWrapper = Mockito.mock(AssertionWrapper.class);
SessionHandler sessionHandler = OIOSAML3Service.getSessionHandlerFactory().getHandler();
@@ -319,24 +349,113 @@ private void assertLogoutRequestRejected(MessageContext messageConte
Mockito.when(sessionHandler.isAuthenticated(session)).thenReturn(true);
Mockito.when(sessionHandler.getAuthnRequest(session)).thenReturn(null);
- // Marshall, deflate and base64 encode as the HTTP-Redirect binding does
+ // Mock HttpServletRequest
+ HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
+ Mockito.when(request.getRequestURL()).thenReturn(new StringBuffer(TestConstants.SP_ASSERTION_CONSUMER_URL));
+ Mockito.when(request.getSession()).thenReturn(session);
+ Mockito.when(request.getMethod()).thenReturn("GET");
+
+ if (signedOnQueryString) {
+ IdpUtil.stubRedirectRequest(request, IdpUtil.encodeAsRedirectUrl(messageContext));
+ } else {
+ Mockito.when(request.getParameter("RelayState")).thenReturn(null);
+ Mockito.when(request.getParameter("SAMLRequest")).thenReturn(deflateAndEncode(messageContext));
+ }
+
+ // Mock HttpServletResponse
+ ServletOutputStream outputStreamMock = Mockito.mock(ServletOutputStream.class);
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+ Mockito.when(response.getOutputStream()).thenReturn(outputStreamMock);
+
+ new LogoutRequestHandler().handleGet(request, response);
+
+ Mockito.verify(sessionHandler).logout(session, assertionWrapper);
+ Mockito.verify(session).invalidate();
+ }
+
+ /**
+ * Run the action with the library pointed at the given IdP metadata, as a deployment is while the IdP
+ * rotates its signing key.
+ */
+ private void withIdPMetadata(String metadata, TestAction action) throws Exception {
+ String originalMetadataFile = OIOSAML3Service.getConfig().getIdpMetadataFile();
+ OIOSAML3Service.getConfig().setIdpMetadataFile(TestConstants.writeIdpMetadataFile(metadata));
+ IdPMetadataService.getInstance().clear(TestConstants.IDP_ENTITY_ID);
+
+ try {
+ action.run();
+ } finally {
+ OIOSAML3Service.getConfig().setIdpMetadataFile(originalMetadataFile);
+ IdPMetadataService.getInstance().clear(TestConstants.IDP_ENTITY_ID);
+ }
+ }
+
+ @FunctionalInterface
+ private interface TestAction {
+ void run() throws Exception;
+ }
+
+ /**
+ * Marshall, deflate and base64 encode the message as the HTTP-Redirect binding does.
+ */
+ private static String deflateAndEncode(MessageContext messageContext) throws Exception {
Element marshalledMessage = XMLObjectSupport.marshall(messageContext.getMessage());
+
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
DeflaterOutputStream deflaterStream = new DeflaterOutputStream(bytesOut, new Deflater(8, true));
deflaterStream.write(SerializeSupport.nodeToString(marshalledMessage).getBytes("UTF-8"));
deflaterStream.finish();
+ return Base64Support.encode(bytesOut.toByteArray(), Base64Support.UNCHUNKED);
+ }
+
+ /**
+ * Metadata where the certificate actually used for signing is preceded by another one, as it is while the
+ * IdP rotates its signing key.
+ */
+ private static String metadataWithTwoSigningCertificates() throws Exception {
+ String otherCertificate = IdpUtil.getIdpCertificateBase64(false);
+
+ return TestConstants.IDP_METADATA.replaceFirst("",
+ ""
+ + otherCertificate + "");
+ }
+
+ /**
+ * Metadata publishing no key for signing, as it is when every published certificate has been revoked.
+ */
+ private static String metadataWithoutSigningCertificate() {
+ return TestConstants.IDP_METADATA.replace("", "");
+ }
+
+ /**
+ * Send the LogoutRequest to the handler and require that it is refused without any session being touched.
+ */
+ private void assertLogoutRequestRejected(MessageContext messageContext) throws Exception {
+ assertLogoutRequestRejected(messageContext, ExternalException.class);
+ }
+
+ private void assertLogoutRequestRejected(MessageContext messageContext, Class extends Exception> expected) throws Exception {
+ HttpSession session = Mockito.mock(HttpSession.class);
+ AssertionWrapper assertionWrapper = Mockito.mock(AssertionWrapper.class);
+ SessionHandler sessionHandler = OIOSAML3Service.getSessionHandlerFactory().getHandler();
+
+ String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
+ Mockito.when(sessionHandler.getAssertion(sessionIndex)).thenReturn(assertionWrapper);
+ Mockito.when(sessionHandler.isAuthenticated(session)).thenReturn(true);
+ Mockito.when(sessionHandler.getAuthnRequest(session)).thenReturn(null);
+
// Mock HttpServletRequest
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getRequestURL()).thenReturn(new StringBuffer(TestConstants.SP_ASSERTION_CONSUMER_URL));
Mockito.when(request.getSession()).thenReturn(session);
Mockito.when(request.getMethod()).thenReturn("GET");
Mockito.when(request.getParameter("RelayState")).thenReturn(null);
- Mockito.when(request.getParameter("SAMLRequest")).thenReturn(Base64Support.encode(bytesOut.toByteArray(), Base64Support.UNCHUNKED));
+ Mockito.when(request.getParameter("SAMLRequest")).thenReturn(deflateAndEncode(messageContext));
HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
- Assertions.assertThrows(ExternalException.class, () -> new LogoutRequestHandler().handleGet(request, response));
+ Assertions.assertThrows(expected, () -> new LogoutRequestHandler().handleGet(request, response));
// The session handler mock is shared between tests, so verify against this tests own session
Mockito.verify(sessionHandler, Mockito.never()).logout(Mockito.eq(session), Mockito.any(AssertionWrapper.class));
@@ -376,9 +495,9 @@ public void testLogoutRequestWhenNotLoggedIn() throws InternalException, IOExcep
@Test
public void testSOAPLogoutRequestWhenNotLoggedIn() throws Exception {
// Create LogoutRequest
- String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ String nameID = NAME_ID;
MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(nameID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
- String sessionIndex = ((LogoutRequest)messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
+ String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
// Marshall and serialize
Element marshalledMessage = XMLObjectSupport.marshall(messageContext.getMessage());
@@ -402,7 +521,7 @@ public void testSOAPLogoutRequestWhenNotLoggedIn() throws Exception {
Mockito.when(request.getMethod()).thenReturn("POST"); // Method: GET
Mockito.when(request.getContentType()).thenReturn("text/xml");
Mockito.when(request.getHeader("SOAPAction")).thenReturn("SOAPAction");
- Mockito.when(request.getInputStream()).thenReturn(new ServletInputStream(){
+ Mockito.when(request.getInputStream()).thenReturn(new ServletInputStream() {
public int read() throws IOException {
return inputStream.read();
}
@@ -461,9 +580,9 @@ public void setReadListener(ReadListener readListener) {
@Test
public void testIdPLogoutRequestWhenNotLoggedIn() throws Exception {
// Create LogoutRequest
- String nameID = "https://data.gov.dk/model/core/eid/person/uuid/37a5a1aa-67ce-4f70-b7c0-b8e678d585f7";
+ String nameID = NAME_ID;
MessageContext messageContext = IdpUtil.createMessageWithLogoutRequest(nameID, NameID.PERSISTENT, TestConstants.SP_LOGOUT_REQUEST_URL);
- String sessionIndex = ((LogoutRequest)messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
+ String sessionIndex = ((LogoutRequest) messageContext.getMessage()).getSessionIndexes().get(0).getSessionIndex();
// Marshall and serialize
Element marshalledMessage = XMLObjectSupport.marshall(messageContext.getMessage());
@@ -506,7 +625,7 @@ public void testIdPLogoutRequestWhenNotLoggedIn() throws Exception {
Mockito.verify(sessionHandler, Mockito.never()).getAssertion(session);
Mockito.verify(sessionHandler, Mockito.times(1)).getAssertion(sessionIndex);
- Mockito.verify(sessionHandler, Mockito.never()).logout(Mockito.eq(session),Mockito.any(AssertionWrapper.class));
+ Mockito.verify(sessionHandler, Mockito.never()).logout(Mockito.eq(session), Mockito.any(AssertionWrapper.class));
Mockito.verify(session).invalidate();
Mockito.verify(outputStreamMock).flush(); //Verify that something is sent to the IdP
}
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/session/SessionDestroyListenerTest.java b/oiosaml/src/test/java/dk/gov/oio/saml/session/SessionDestroyListenerTest.java
index b96e313..7072215 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/session/SessionDestroyListenerTest.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/session/SessionDestroyListenerTest.java
@@ -26,7 +26,7 @@ void beforeEach() throws Exception {
.setServletRoutingPathSuffixLogoutResponse(TestConstants.SP_ROUTING_LOGOUT_RESPONSE)
.setServletRoutingPathSuffixAssertion(TestConstants.SP_ROUTING_ASSERTION)
.setIdpEntityID(TestConstants.IDP_ENTITY_ID)
- .setIdpMetadataUrl(TestConstants.IDP_METADATA_URL)
+ .setIdpMetadataFile(TestConstants.idpMetadataFile())
.setSessionHandlerFactoryClassName(TestSessionHandlerFactory.class.getName())
.setKeystoreLocation("sp.pfx")
.setKeystorePassword("Test1234")
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/util/IdpUtil.java b/oiosaml/src/test/java/dk/gov/oio/saml/util/IdpUtil.java
index ca55c94..bb6a20d 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/util/IdpUtil.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/util/IdpUtil.java
@@ -495,6 +495,14 @@ private static T buildSAMLObject(final Class clazz) {
return object;
}
+ /**
+ * Base64 encoded certificate of the IdP signing key, or of the unrelated key used for invalid
+ * signatures, for building metadata variants.
+ */
+ public static String getIdpCertificateBase64(boolean validSignature) throws Exception {
+ return java.util.Base64.getEncoder().encodeToString(getX509Credential(validSignature).getEntityCertificate().getEncoded());
+ }
+
private static BasicX509Credential getX509Credential(boolean validSignature) throws Exception {
String resourceName = (validSignature) ? "idp.pfx" : "idp-invalid.pfx";
diff --git a/oiosaml/src/test/java/dk/gov/oio/saml/util/TestConstants.java b/oiosaml/src/test/java/dk/gov/oio/saml/util/TestConstants.java
index 8b3b369..149b600 100644
--- a/oiosaml/src/test/java/dk/gov/oio/saml/util/TestConstants.java
+++ b/oiosaml/src/test/java/dk/gov/oio/saml/util/TestConstants.java
@@ -18,6 +18,26 @@ public class TestConstants {
public static final String IDP_ENTITY_ID = "http://mockidp.localhost";
public static final String IDP_METADATA_URL = "http://localhost:8081/saml/metadata";
+
+ /**
+ * IdP metadata is deployed as a file, so tests hand the library a file rather than a URL.
+ */
+ public static String idpMetadataFile() {
+ return writeIdpMetadataFile(IDP_METADATA);
+ }
+
+ public static String writeIdpMetadataFile(String metadata) {
+ try {
+ java.io.File file = java.io.File.createTempFile("oiosaml-test-idp-metadata", ".xml");
+ file.deleteOnExit();
+ java.nio.file.Files.write(file.toPath(), metadata.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+
+ return file.getAbsolutePath();
+ }
+ catch (java.io.IOException e) {
+ throw new IllegalStateException("Could not write test metadata file", e);
+ }
+ }
public static final String IDP_LOGOUT_REQUEST_URL = "http://localhost:8081/saml/logout";
public static final String IDP_LOGOUT_RESPONSE_URL = "http://localhost:8081/saml/logout/response";