From a3a227d323b98842517a350ce24fdbe1590e2871 Mon Sep 17 00:00:00 2001 From: Thomas Nymand Date: Thu, 13 Aug 2026 13:47:55 +0200 Subject: [PATCH] REF-27: Remove validation of the specVersion attribute value in assertions validateAttributeStatement required specVersion to equal "OIO-SAML-3.0". The OIOSAML 4.0.0 profile changes that value to https://data.gov.dk/saml/profile/oio/4.0.0/, so assertions issued under the newer profile were rejected, and every future profile revision would need a new release. The value announces which profile version the assertion was issued under, and nothing in the profiles asks the SP to check it. Only the presence of the attribute is required now, as mandated by [OIO-AP-01] in both 3.0.3 and 4.0.0. Constants.SPEC_VER_VAL is removed as unused; it is a compile time constant, so existing binaries are unaffected, but recompiling against it will fail. Tests cover an assertion carrying the 4.0.0 value and one missing the attribute. --- .../AssertionValidationService.java | 9 ++-- .../java/dk/gov/oio/saml/util/Constants.java | 1 - .../AssertionValidationServiceTest.java | 54 +++++++++++++++++++ .../java/dk/gov/oio/saml/util/IdpUtil.java | 34 ++++++++++-- .../dk/gov/oio/saml/util/TestConstants.java | 4 ++ 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/oiosaml/src/main/java/dk/gov/oio/saml/service/validation/AssertionValidationService.java b/oiosaml/src/main/java/dk/gov/oio/saml/service/validation/AssertionValidationService.java index 75262b6..031fbf2 100644 --- a/oiosaml/src/main/java/dk/gov/oio/saml/service/validation/AssertionValidationService.java +++ b/oiosaml/src/main/java/dk/gov/oio/saml/service/validation/AssertionValidationService.java @@ -196,11 +196,14 @@ private void validateAssertion(Assertion assertion, AuthnRequestWrapper authnReq } private void validateAttributeStatement(Map attributes, boolean isProfessional) throws AssertionValidationException { - // SpecVer + // SpecVer is mandatory in the OIOSAML attribute profiles, but its value identifies the profile + // version the assertion was issued under and changes between profile versions, so only the + // presence of the attribute is required here String specVersion = attributes.get(Constants.SPEC_VER); - if (!Constants.SPEC_VER_VAL.equals(specVersion)) { - throw new AssertionValidationException("specVersion Was: " + specVersion + " Expected: " + Constants.SPEC_VER_VAL); + if (specVersion == null || specVersion.isEmpty()) { + throw new AssertionValidationException("Assertions MUST contain the specVersion attribute " + Constants.SPEC_VER); } + log.debug("Assertion issued under OIOSAML profile version '{}'", specVersion); // Professional if (isProfessional) { 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 c080683..6ac31bb 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 @@ -69,7 +69,6 @@ public class Constants { // SAML Attributes constants public static final String SPEC_VER = "https://data.gov.dk/model/core/specVersion"; - public static final String SPEC_VER_VAL = "OIO-SAML-3.0"; public static final String PRIVILEGE_ATTRIBUTE = "https://data.gov.dk/model/core/eid/privilegesIntermediate"; public static final String LOA = "https://data.gov.dk/concept/core/nsis/loa"; public static final String CVR_NUMBER = "https://data.gov.dk/model/core/eid/professional/cvr"; 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 070fa10..9dded8c 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 @@ -58,6 +58,60 @@ public void testValidateCorrectAssertion() throws Exception { validationService.validate(request, messageContext, response, assertion, new AuthnRequestWrapper(authnRequest, NSISLevel.SUBSTANTIAL, "")); } + @DisplayName("Test that validator will pass an assertion issued under a newer OIOSAML profile version") + @Test + public void testValidateAssertionWithNewerSpecVersion() throws Exception { + AssertionValidationService validationService = new AssertionValidationService(); + + // 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, with the specVersion value used by OIOSAML 4.0.0 + 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, TestConstants.SPEC_VERSION_OIOSAML_40); + Response response = (Response) messageContext.getMessage(); + + AssertionService assertionService = new AssertionService(); + Assertion assertion = assertionService.getAssertion(response); + + // Validate + validationService.validate(request, messageContext, response, assertion, new AuthnRequestWrapper(authnRequest, NSISLevel.SUBSTANTIAL, "")); + } + + @DisplayName("Test that validator will fail an assertion without a specVersion attribute") + @Test + public void testFailAssertionWithoutSpecVersion() throws Exception { + AssertionValidationService validationService = new AssertionValidationService(); + + // 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, without the mandatory specVersion attribute + 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, null); + Response response = (Response) messageContext.getMessage(); + + AssertionService assertionService = new AssertionService(); + Assertion assertion = assertionService.getAssertion(response); + + // Validate, should fail, specVersion is mandatory + Assertions.assertThrows(AssertionValidationException.class, () -> { + validationService.validate(request, messageContext, response, assertion, new AuthnRequestWrapper(authnRequest, NSISLevel.SUBSTANTIAL, "")); + }); + } + @DisplayName("Test that validator will fail an assertion with the wrong destination") @Test public void testFailAssertionWithWrongDestination() throws Exception { 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 427dcb7..c0246af 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 @@ -58,8 +58,20 @@ public static MessageContext createMessageWithAssertion( String recipientEntityId, String assertionConsumerUrl, String inResponseToId) throws Exception { + return createMessageWithAssertion(encrypted, validCert, validSignature, subjectNameID, recipientEntityId, assertionConsumerUrl, inResponseToId, TestConstants.SPEC_VERSION_OIOSAML_30); + } + + public static MessageContext createMessageWithAssertion( + boolean encrypted, + boolean validCert, + boolean validSignature, + String subjectNameID, + String recipientEntityId, + String assertionConsumerUrl, + String inResponseToId, + String specVersion) throws Exception { // Create proxy Response - Response response = createResponse(encrypted, validCert, validSignature, subjectNameID, recipientEntityId, assertionConsumerUrl, inResponseToId); + Response response = createResponse(encrypted, validCert, validSignature, subjectNameID, recipientEntityId, assertionConsumerUrl, inResponseToId, specVersion); // Build Proxy MessageContext and add response MessageContext messageContext = new MessageContext<>(); @@ -89,6 +101,18 @@ public static Response createResponse( String recipientEntityId, String assertionConsumerUrl, String inResponseToId) throws Exception { + return createResponse(encrypted, validCert, validSignature, subjectNameID, recipientEntityId, assertionConsumerUrl, inResponseToId, TestConstants.SPEC_VERSION_OIOSAML_30); + } + + public static Response createResponse( + boolean encrypted, + boolean validCert, + boolean validSignature, + String subjectNameID, + String recipientEntityId, + String assertionConsumerUrl, + String inResponseToId, + String specVersion) throws Exception { DateTime issueInstant = new DateTime(); @@ -110,7 +134,7 @@ public static Response createResponse( status.setStatusCode(statusCode); response.setStatus(status); - Assertion assertion = createAssertion(issueInstant, subjectNameID, recipientEntityId, assertionConsumerUrl); + Assertion assertion = createAssertion(issueInstant, subjectNameID, recipientEntityId, assertionConsumerUrl, specVersion); SignAssertion(assertion, validSignature); if (encrypted) { EncryptedAssertion encryptedAssertion = encryptAssertion(assertion, validCert); @@ -282,7 +306,7 @@ private static void SignAssertion(Assertion assertion, boolean validSignature) t Signer.signObject(signature); } - private static Assertion createAssertion(DateTime issueInstant, String subjectNameID, String recipientEntityId, String assertionConsumerUrl) { + private static Assertion createAssertion(DateTime issueInstant, String subjectNameID, String recipientEntityId, String assertionConsumerUrl, String specVersion) { RandomIdentifierGenerationStrategy secureRandomIdGenerator = new RandomIdentifierGenerationStrategy(); String id = secureRandomIdGenerator.generateIdentifier(); @@ -310,7 +334,9 @@ private static Assertion createAssertion(DateTime issueInstant, String subjectNa AttributeStatement attributeStatement = buildSAMLObject(AttributeStatement.class); List attributes = attributeStatement.getAttributes(); - attributes.add(createSimpleAttribute("https://data.gov.dk/model/core/specVersion", "OIO-SAML-3.0")); + if (specVersion != null) { + attributes.add(createSimpleAttribute(Constants.SPEC_VER, specVersion)); + } attributes.add(createSimpleAttribute("https://data.gov.dk/concept/core/nsis/loa", NSISLevel.SUBSTANTIAL.getName())); assertion.getAttributeStatements().add(attributeStatement); 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 5424a13..0953299 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 @@ -147,6 +147,10 @@ public class TestConstants { " \n" + ""; public static final String BAD_SP_ASSERTION_CONSUMER_URL = "http://localhost:8080/sso"; + + // Value of the specVersion attribute in the OIOSAML Web SSO profiles, see [OIO-ALG-01] chapter 6 + public static final String SPEC_VERSION_OIOSAML_30 = "OIO-SAML-3.0"; + public static final String SPEC_VERSION_OIOSAML_40 = "https://data.gov.dk/saml/profile/oio/4.0.0/"; public static final String VALID_CERTIFICATE = "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";