diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java index 7e47b279a2cd0378fdc61d897743701857a5dc41..ef3f560a365812966630d2de2b0e7293373321c7 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java @@ -25,6 +25,7 @@ package de.ozgcloud.token.saml; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -144,10 +145,12 @@ public class SamlTokenValidationService { } Map<String, String> adjustPostfachIdAttribute(Map<String, String> tokenAttributes, Response token) { - if (tokenValidationProperty.isUseIdAsPostfachId()) { - tokenAttributes.put(TokenAttribute.POSTFACH_ID_KEY, token.getID()); + if (!tokenValidationProperty.isUseIdAsPostfachId()) { + return tokenAttributes; } - return tokenAttributes; + var map = new HashMap<>(tokenAttributes); + map.put(TokenAttribute.POSTFACH_ID_KEY, token.getID()); + return map; } TokenAttribute buildTokenAttribute(Map.Entry<String, String> attribute) { diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java index 5e55108e9014a798e8e918de8e32d37a9fddba6c..050d50edf052fd0437c9b2120edd27684ee2c451 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java @@ -1,5 +1,30 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ package de.ozgcloud.token; +import java.util.Map; + import com.thedeanda.lorem.LoremIpsum; public class TokenAttributeTestFactory { @@ -17,4 +42,7 @@ public class TokenAttributeTestFactory { .value(VALUE); } + public static Map<String, String> asMap() { + return Map.of(NAME, VALUE); + } } diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7f0fd52a2b42bf6c5184810e7baf2995563cb980 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.token.saml; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.schema.XSAny; +import org.opensaml.core.xml.schema.XSBoolean; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.opensaml.core.xml.schema.XSInteger; +import org.opensaml.core.xml.schema.XSString; +import org.opensaml.saml.saml2.core.Assertion; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.core.AttributeStatement; +import org.opensaml.saml.saml2.core.EncryptedAssertion; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.Statement; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.token.TokenAttribute; +import de.ozgcloud.token.TokenAttributeTestFactory; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import lombok.SneakyThrows; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; + +class SamlTokenValidationServiceTest { + + @Spy + @InjectMocks + private SamlTokenValidationService service; + + @Mock + private SignatureTrustEngine signatureTrustEngine; + @Mock + private Decrypter decrypter; + @Mock + private SAMLSignatureProfileValidator profileValidator; + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private CriteriaSet verificationCriteria; + + @Nested + class TestValidate { + + private static final Map<String, String> TOKEN_ATTRIBUTES_MAP = Map.of("key", "value"); + private static final TokenAttribute TOKEN_ATTRIBUTE = TokenAttributeTestFactory.create(); + + @Mock + private Response token; + + @BeforeEach + void init() { + doNothing().when(service).validateToken(any()); + doReturn(TOKEN_ATTRIBUTES_MAP).when(service).decryptAttributes(any()); + doReturn(Set.of(TOKEN_ATTRIBUTE)).when(service).buildTokenAttributes(any(), any()); + } + + @Test + void shouldCallValidateToken() { + validate(); + + verify(service).validate(token); + } + + @Test + void shouldCallDecryptAttributes() { + validate(); + + verify(service).decryptAttributes(token); + } + + @Test + void shouldCallBuildTokenAttributes() { + validate(); + + verify(service).buildTokenAttributes(TOKEN_ATTRIBUTES_MAP, token); + } + + @Test + void shouldReturnResult() { + var result = validate(); + + assertThat(result).containsExactly(TOKEN_ATTRIBUTE); + } + + private Set<TokenAttribute> validate() { + return service.validate(token); + } + } + + @Nested + class TestValidateToken { + + @Mock + private Response token; + + @Nested + class TestValidateSuccessfully { + + @Mock + private Signature signature; + + @BeforeEach + void init() { + when(token.getSignature()).thenReturn(signature); + doReturn(true).when(service).validateSignature(any()); + } + + @Test + void shouldCallValidateSignatureProfile() { + validateToken(); + + verify(service).validateSignatureProfile(signature); + } + + @Test + void shouldValidateToken() { + validateToken(); + + Assertions.assertDoesNotThrow(TestValidateToken.this::validateToken); + } + } + + @Nested + class TestValidationFailed { + + @Mock + private Signature signature; + + @Test + void shouldThrowWhenMissingSignature() { + assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken); + } + + @Test + void shouldThrowWhenInvalidSignatureProfile() { + when(token.getSignature()).thenReturn(signature); + when(service.validateSignature(any())).thenReturn(false); + + assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken); + } + } + + private void validateToken() { + service.validateToken(token); + } + } + + @Nested + class TestValidateSignatureProfile { + + @Mock + private Signature signature; + + @Test + @SneakyThrows + void shouldCallProfileValidator() { + service.validateSignatureProfile(signature); + + verify(profileValidator).validate(signature); + } + } + + @Nested + class TestValidateSignature { + + @Mock + private Signature signature; + + @Test + @SneakyThrows + void shouldCallSignatureTrustEngine() { + service.validateSignature(signature); + + verify(signatureTrustEngine).validate(signature, verificationCriteria); + } + } + + @Nested + class TestDecryptAttributes { + + @Mock + private Response token; + @Mock + private EncryptedAssertion encryptedAssertion; + @Mock + private Assertion assertion; + @Mock + private AttributeStatement attributeStatement; + @Mock + private Attribute attribute; + + @BeforeEach + void init() { + doReturn(assertion).when(service).decryptAssertion(any()); + doReturn(Optional.of(attributeStatement)).when(service).getAttributeStatement(any()); + doReturn(TokenAttributeTestFactory.VALUE).when(service).getAttributeValues(attribute); + when(token.getEncryptedAssertions()).thenReturn(List.of(encryptedAssertion)); + when(attributeStatement.getAttributes()).thenReturn(List.of(attribute)); + when(attribute.getName()).thenReturn(TokenAttributeTestFactory.NAME); + } + + @Test + void shouldCallDecryptAssertion() { + decryptAttributes(); + + verify(service).decryptAssertion(encryptedAssertion); + } + + @Test + void shouldCallGetAttributeStatement() { + decryptAttributes(); + + verify(service).getAttributeStatement(assertion); + } + + @Test + void shouldCallGetAttributeValues() { + decryptAttributes(); + + verify(service).getAttributeValues(attribute); + } + + @Test + void shouldReturnAttributes() { + var result = decryptAttributes(); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.asMap()); + } + + private Map<String, String> decryptAttributes() { + return service.decryptAttributes(token); + } + } + + @Nested + class TestDecryptAssertion { + + @Mock + private EncryptedAssertion encryptedAssertion; + @Mock + private Assertion assertion; + + @SneakyThrows + @BeforeEach + void init() { + doReturn(assertion).when(decrypter).decrypt(any(EncryptedAssertion.class)); + } + + @Test + @SneakyThrows + void shouldDecryptAssertion() { + service.decryptAssertion(encryptedAssertion); + + verify(decrypter).decrypt(encryptedAssertion); + } + + @Test + void shouldReturnResult() { + var result = service.decryptAssertion(encryptedAssertion); + + assertThat(result).isEqualTo(assertion); + } + } + + @Nested + class TestGetAttributeStatement { + + @Mock + private Assertion assertion; + @Mock + private AttributeStatement attributeStatement; + @Mock + private Statement statement; + + @Test + void shouldReturnAttributeStatement() { + when(assertion.getStatements()).thenReturn(List.of(attributeStatement)); + + var result = service.getAttributeStatement(assertion); + + assertThat(result).contains(attributeStatement); + } + + @Test + void shouldReturnEmptyOptional() { + when(assertion.getStatements()).thenReturn(List.of(statement)); + + var result = service.getAttributeStatement(assertion); + + assertThat(result).isEmpty(); + } + } + + @Nested + class TestGetAttributeValues { + + private static final String VALUE1 = LoremIpsum.getInstance().getWords(1); + private static final String VALUE2 = LoremIpsum.getInstance().getWords(1); + + @Mock + private Attribute attribute; + @Mock + private XMLObject attributeValue1; + @Mock + private XMLObject attributeValue2; + + @BeforeEach + void init() { + doReturn(VALUE1).when(service).getAttributeValue(attributeValue1); + } + + @Test + void shouldCallGetAttributeValue() { + when(attribute.getAttributeValues()).thenReturn(List.of(attributeValue1)); + + service.getAttributeValues(attribute); + + verify(service).getAttributeValue(attributeValue1); + } + + @Test + void shouldReturnAttributeValues() { + doReturn(VALUE2).when(service).getAttributeValue(attributeValue2); + when(attribute.getAttributeValues()).thenReturn(List.of(attributeValue1, attributeValue2)); + + var result = service.getAttributeValues(attribute); + + assertThat(result).isEqualTo(VALUE1 + ";" + VALUE2); + } + } + + @Nested + class TestGetAttributeValue { + + @Test + void shouldReturnEmptyString() { + var result = service.getAttributeValue(null); + + assertThat(result).isEmpty(); + } + + @Test + void shouldReturnWhenXSString() { + XMLObject attrValue = when(mock(XSString.class).getValue()).thenReturn(TokenAttributeTestFactory.VALUE).getMock(); + + var result = service.getAttributeValue(attrValue); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.VALUE); + } + + @Test + void shouldReturnWhenXSAny() { + XMLObject attrValue = when(mock(XSAny.class).getTextContent()).thenReturn(TokenAttributeTestFactory.VALUE).getMock(); + + var result = service.getAttributeValue(attrValue); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.VALUE); + } + + @Test + void shouldReturnWhenXSInteger() { + var value = new Random().nextInt(); + XMLObject attrValue = when(mock(XSInteger.class).getValue()).thenReturn(value).getMock(); + + var result = service.getAttributeValue(attrValue); + + assertThat(result).isEqualTo(String.valueOf(value)); + } + + @Test + void shouldReturnWhenXSBoolean() { + var value = String.valueOf(new Random().nextBoolean()); + XSBooleanValue booleanValue = when(mock(XSBooleanValue.class).toString()).thenReturn(value).getMock(); + XMLObject attrValue = when(mock(XSBoolean.class).getValue()).thenReturn(booleanValue).getMock(); + + var result = service.getAttributeValue(attrValue); + + assertThat(result).isEqualTo(value); + } + } + + @Nested + class TestBuildTokenAttributes { + + private static final Map.Entry<String, String> ADJUSTED_TOKEN_ATTRIBUTES_ENTRY = Map.entry("key", "value"); + private static final Map<String, String> ADJUSTED_TOKEN_ATTRIBUTES_MAP = Map.ofEntries(ADJUSTED_TOKEN_ATTRIBUTES_ENTRY); + private static final TokenAttribute TOKEN_ATTRIBUTE = TokenAttributeTestFactory.create(); + + @Mock + private Response token; + + @BeforeEach + void init() { + doReturn(ADJUSTED_TOKEN_ATTRIBUTES_MAP).when(service).adjustPostfachIdAttribute(any(), any()); + doReturn(TOKEN_ATTRIBUTE).when(service).buildTokenAttribute(any()); + } + + @Test + void shouldCallAdjustPostfachIdAttribute() { + buildTokenAttributes(); + + verify(service).adjustPostfachIdAttribute(TokenAttributeTestFactory.asMap(), token); + } + + @Test + void shouldCallBuildTokenAttribute() { + buildTokenAttributes(); + + verify(service).buildTokenAttribute(ADJUSTED_TOKEN_ATTRIBUTES_ENTRY); + } + + @Test + void shouldReturnTokenAttributes() { + var result = buildTokenAttributes(); + + assertThat(result).containsExactly(TOKEN_ATTRIBUTE); + } + + private Set<TokenAttribute> buildTokenAttributes() { + return service.buildTokenAttributes(TokenAttributeTestFactory.asMap(), token); + } + } + + @Nested + class TestAdjustPostfachIdAttribute { + + private static final String POSTFACH_ID = LoremIpsum.getInstance().getWords(1); + + @Mock + private Response token; + + @Test + void shouldSetPostfachId() { + when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(true); + when(token.getID()).thenReturn(POSTFACH_ID); + + var result = adjustPostfachIdAttribute(); + + assertThat(result).containsEntry(TokenAttribute.POSTFACH_ID_KEY, POSTFACH_ID); + } + + @Test + void shouldRewritePostfachId() { + when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(true); + var attributeMap = Map.of(TokenAttribute.POSTFACH_ID_KEY, LoremIpsum.getInstance().getWords(1)); + when(token.getID()).thenReturn(POSTFACH_ID); + + var result = service.adjustPostfachIdAttribute(attributeMap, token); + + assertThat(result).containsEntry(TokenAttribute.POSTFACH_ID_KEY, POSTFACH_ID); + } + + @Test + void shouldReturnUnchangedMap() { + when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(false); + var attributeMap = TokenAttributeTestFactory.asMap(); + + var result = service.adjustPostfachIdAttribute(attributeMap, token); + + assertThat(result).isEqualTo(attributeMap); + } + + private Map<String, String> adjustPostfachIdAttribute() { + return service.adjustPostfachIdAttribute(TokenAttributeTestFactory.asMap(), token); + } + } + + @Nested + class TestBuildTokenAttribute { + + @Test + void shouldBuildTokenAttribute() { + var result = service.buildTokenAttribute(Map.entry(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE)); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.create()); + } + } +} \ No newline at end of file