diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/CheckErrorMapper.java b/token-checker-server/src/main/java/de/ozgcloud/token/CheckErrorMapper.java deleted file mode 100644 index 7cd1e55ac39a2f16b9424d6974d3ab6d0a512d0b..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/CheckErrorMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.ozgcloud.token; - -import org.mapstruct.CollectionMappingStrategy; -import org.mapstruct.Mapper; -import org.mapstruct.ReportingPolicy; - -@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) -public interface CheckErrorMapper { - -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlSetting.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java similarity index 59% rename from token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlSetting.java rename to token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java index f0b19123d448effd6f12f2a9b5b31de45a76aedd..82e468eaee1c71c94064514d34fc39d93f57ed9b 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlSetting.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java @@ -18,33 +18,28 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token.saml; - -import java.util.Map; - -import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +package de.ozgcloud.token; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; @Builder @Getter @EqualsAndHashCode -public class SamlSetting { - private SignatureTrustEngine trustEngine; - private CriteriaSet criteriaSet; - private Decrypter decrypter; - private Map<String, String> mappings; - private boolean idIsPostfachId; +public class TokenAttribute { + + public static final String POSTFACH_ID_KEY = "postfachId"; + public static final String TRUST_LEVEL_KEY = "trustLevel"; + + private String name; + private String value; + + public boolean isPostfachId() { + return POSTFACH_ID_KEY.equals(name); + } - @Override - public String toString() { - return "SamlSetting{" + - "mappings=" + mappings + - ", idIsPostKorbMapping=" + idIsPostfachId + - '}'; + public boolean isTrustLevel() { + return TRUST_LEVEL_KEY.equals(name); } } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckService.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckService.java deleted file mode 100644 index e0920b69121760f4e4a611685c3399c800f3f00e..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckService.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2024. - * 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.List; - -import org.apache.commons.lang3.StringUtils; -import org.opensaml.saml.saml2.core.Response; -import org.springframework.stereotype.Service; - -import de.ozgcloud.token.common.errorhandling.TokenVerificationException; -import de.ozgcloud.token.saml.Saml2DecryptionService; -import de.ozgcloud.token.saml.Saml2ParseService; -import de.ozgcloud.token.saml.Saml2VerificationService; -import de.ozgcloud.token.saml.SamlSetting; -import de.ozgcloud.token.saml.SamlServiceRegistry; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class TokenCheckService { - - public static final String POSTFACH_ID_KEY = "postfachId"; - public static final String TRUST_LEVEL_KEY = "trustLevel"; - - private final SamlServiceRegistry samlServiceRegistry; - private final Saml2DecryptionService decryptionService; - private final Saml2ParseService parseService; - private final Saml2VerificationService verificationService; - - public TokenValidationResult checkToken(final String token) { - var errors = verificationService.verify(token); - if (errors.isEmpty()) { - return getCheckTokenResult(token); - } - throw new TokenVerificationException("Errors occurred while checking token", errors); - } - - TokenValidationResult getCheckTokenResult(final String token) { - var response = parseService.parse(token); - var samlSetting = samlServiceRegistry.getService(response.getIssuer().getValue()); - return buildCheckTokenResult(samlSetting, response); - } - - TokenValidationResult buildCheckTokenResult(SamlSetting samlSetting, Response response) { - var decryptedAttributes = decryptionService.decryptAttributes(response, samlSetting); - return TokenValidationResult.builder() - .attributes(decryptedAttributes) - .postfachId(getPostfachId(samlSetting, response, decryptedAttributes)) - .trustLevel(findAttributeByKey(TRUST_LEVEL_KEY, decryptedAttributes, samlSetting)) - .build(); - } - - String getPostfachId(final SamlSetting samlSetting, final Response response, final List<TokenField> decryptedAttributes) { - if (samlSetting.isIdIsPostfachId()) { - return response.getID(); - } else { - return findAttributeByKey(POSTFACH_ID_KEY, decryptedAttributes, samlSetting); - } - } - - String findAttributeByKey(String key, List<TokenField> attributes, SamlSetting samlSetting) { - var name = samlSetting.getMappings().get(key); - - return attributes.stream().filter(attribute -> attribute.getName().equals(name)) - .findFirst().map(TokenField::getValue).orElse(StringUtils.EMPTY); - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationProperties.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationProperties.java index 362a8e8b712a55afdb60fdf5db5ffa7ef963fa2f..6d18c6f5914223919dc66e26ef97326bef2c4466 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationProperties.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationProperties.java @@ -81,7 +81,7 @@ public class TokenValidationProperties { /** * Use the user id as Postkorbhandle. For Muk */ - private boolean userIdAsPostfachId = false; + private boolean useIdAsPostfachId = false; /** * The mappings the PostfachHandle and the TrustLevel. The value of the mapping diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenField.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java similarity index 75% rename from token-checker-server/src/main/java/de/ozgcloud/token/TokenField.java rename to token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java index 34e8156652a6b2d00e40bc7ef0000b420a3506c6..cf1cc499ceded34e6f63f4690d30d455dab0fcaf 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenField.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java @@ -20,14 +20,21 @@ package de.ozgcloud.token; +import java.util.List; + import lombok.Builder; import lombok.Getter; +import lombok.Singular; @Builder @Getter -public class TokenField { +public class TokenValidationResult { - private String name; - private String value; + private final boolean valid; + private final String postfachId; + private final String trustLevel; + @Singular + private final List<TokenAttribute> attributes; + private final String errorMesssage; } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/TokenVerificationException.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/TokenVerificationException.java index 358cf8be8e251c8a13c85272509765a85c6a2cf1..63cd4b78d955e9b1725e14bca76087242bc40718 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/TokenVerificationException.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/TokenVerificationException.java @@ -20,19 +20,22 @@ package de.ozgcloud.token.common.errorhandling; -import java.util.List; - -import org.springframework.security.saml2.core.Saml2Error; - import de.ozgcloud.common.errorhandling.TechnicalException; import lombok.Getter; @Getter public class TokenVerificationException extends TechnicalException { - private final List<Saml2Error> errorList; - public TokenVerificationException(final String msg, final List<Saml2Error> errorList) { + public TokenVerificationException(String msg) { super(msg); - this.errorList = errorList; + } + + public TokenVerificationException(String msg, Throwable exception) { + super(msg, exception); + } + + @Override + public String getMessage() { + return "[SAML]" + super.getMessage(); } } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2DecryptionService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2DecryptionService.java deleted file mode 100644 index 18a81ddf7c022942563ae20526768bd787aa6f6c..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2DecryptionService.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2024. - * 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 java.util.List; -import java.util.stream.Collectors; - -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.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.encryption.Decrypter; -import org.opensaml.xmlsec.encryption.support.DecryptionException; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.stereotype.Service; - -import de.ozgcloud.token.TokenField; -import lombok.NoArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@Service -@NoArgsConstructor -public class Saml2DecryptionService { - - public List<TokenField> decryptAttributes(Response response, SamlSetting samlSetting) { - decryptResponseElements(response, samlSetting.getDecrypter()); - - return getAttributes(response); - } - - void decryptResponseElements(Response response, Decrypter decrypter) { - response.getEncryptedAssertions().stream() - .map(encryptedAssertion -> decryptAssertion(encryptedAssertion, decrypter)) - .forEach(assertion -> response.getAssertions().add(assertion)); - } - - Assertion decryptAssertion(EncryptedAssertion assertion, Decrypter decrypter) { - try { - return decrypter.decrypt(assertion); - } catch (DecryptionException ex) { - throw new Saml2Exception(ex); - } - } - - List<TokenField> getAttributes(Response response) { - return getAttributeStatement(response).getAttributes().stream().map(this::extractNameAndValue) - .toList(); - } - - private AttributeStatement getAttributeStatement(Response response) { - return (AttributeStatement) response.getAssertions().getFirst().getStatements().get(1); - } - - private TokenField extractNameAndValue(Attribute attribute) { - return TokenField.builder().name(attribute.getName()).value(getAttributeValues(attribute)).build(); - } - - String getAttributeValues(Attribute attribute) { - var values = attribute.getAttributeValues(); - return values.stream().map(this::getAttributeValue).collect(Collectors.joining(";")); - } - - String getAttributeValue(XMLObject value) { - return switch (value) { - case XSString xsString -> xsString.getValue(); - case XSAny xsAny -> xsAny.getTextContent(); - case XSInteger xsInteger -> String.valueOf(xsInteger.getValue()); - case XSBoolean xsBoolean -> String.valueOf(xsBoolean.getValue()); - case null -> ""; - default -> { - LOG.warn("Unknown value type received: {}", value.getClass().getName()); - yield value.toString(); - } - }; - } - -} \ No newline at end of file diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2ParseService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2ParseService.java deleted file mode 100644 index a381bde4a41cbe9e0c0d941c54c6cbf402893b59..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2ParseService.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2024. - * 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 java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.io.UnmarshallingException; -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.stereotype.Service; - -import de.ozgcloud.common.errorhandling.TechnicalException; -import lombok.RequiredArgsConstructor; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import net.shibboleth.utilities.java.support.xml.XMLParserException; - -@Service -@RequiredArgsConstructor -public class Saml2ParseService { - private final ParserPool parserPool; - private final ResponseUnmarshaller unmarshaller; - - public Response parse(String request) { - try { - return (Response) createXmlObject(request.getBytes(StandardCharsets.UTF_8)); - } catch (IOException e) { - throw new TechnicalException("Error on creating XmlObject!", e); - } - } - - XMLObject createXmlObject(byte[] requestBytes) throws IOException { - - try (var inputStream = new ByteArrayInputStream(requestBytes);) { - var element = parserPool.parse(inputStream).getDocumentElement(); - return unmarshaller.unmarshall(element); - } catch (XMLParserException | UnmarshallingException e) { - throw new Saml2Exception("Failed to deserialize LogoutRequest!", e); - } - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2VerificationService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2VerificationService.java deleted file mode 100644 index d6cd3b7fdc9a9cb85881acd03b17494d0c3d284a..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2VerificationService.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2024. - * 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 java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import org.opensaml.saml.saml2.core.Response; -import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; -import org.opensaml.security.SecurityException; -import org.opensaml.xmlsec.signature.support.SignatureException; -import org.springframework.security.saml2.core.Saml2Error; -import org.springframework.security.saml2.core.Saml2ErrorCodes; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@RequiredArgsConstructor -@Service -public class Saml2VerificationService { - public static final String INVALID_SIGNATURE = "Invalid signature for object!"; - public static final String ERROR_VALIDATING_SIGNATURE = "Error on validating signature!"; - public static final String INVALID_SIGNATURE_PROFILE = "Invalid signature profile for object!"; - public static final String SIGNATURE_MISSING = "Signature missing!"; - static final String FORMAT = " [%s]: "; - - private final Saml2ParseService parser; - private final SamlServiceRegistry samlServiceRegistry; - - private final SAMLSignatureProfileValidator profileValidator; - - public List<Saml2Error> verify(String samlToken) { - var response = parser.parse(samlToken); - return getSaml2Errors(response); - } - - List<Saml2Error> getSaml2Errors(Response response) { - List<Saml2Error> errors = new ArrayList<>(); - - if (response.isSigned()) { - validateProfile(response, errors); - validateSignature(response, errors); - } else { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, SIGNATURE_MISSING)); - } - return Collections.unmodifiableList(errors); - } - - void validateProfile(Response response, List<Saml2Error> errors) { - try { - profileValidator.validate(Objects.requireNonNull(response.getSignature())); - } catch (SignatureException ex) { - LOG.warn("Error validating SAML Token: ", ex); - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE_PROFILE + FORMAT.formatted(response.getID()))); - } - } - - void validateSignature(Response response, List<Saml2Error> errors) { - var samlSetting = samlServiceRegistry.getService(response.getIssuer().getValue()); - try { - if (!samlSetting.getTrustEngine().validate(Objects.requireNonNull(response.getSignature()), samlSetting.getCriteriaSet())) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE + FORMAT.formatted(response.getID()))); - } - } catch (SecurityException e) { - LOG.warn("Error validating SAML Token: ", e); - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, ERROR_VALIDATING_SIGNATURE + FORMAT.formatted(response.getID()))); - } - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfiguration.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfiguration.java index 2376beab0c93654b3233ef461e561d50fab9f730..b0148c85562b13f456f4980f9dc7c8f25ba989e2 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfiguration.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfiguration.java @@ -52,7 +52,6 @@ public class SamlConfiguration { .signatureTrustEngine(samlTrustEngineFactory.buildSamlTrustEngine(tokenValidationProperty)) .decrypter(samlDecrypterFactory.buildDecrypter(tokenValidationProperty)) .verificationCriteria(buildVerificationCriteria(tokenValidationProperty.getIdpEntityId())) - .userIdAsPostfachId(tokenValidationProperty.isUserIdAsPostfachId()) .build(); } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenService.java new file mode 100644 index 0000000000000000000000000000000000000000..819567584d0705d59f31af49a0643d1f379257ec --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenService.java @@ -0,0 +1,112 @@ +/* + * 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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Optional; +import java.util.Set; + +import org.opensaml.core.xml.io.UnmarshallingException; +import org.opensaml.saml.saml2.core.Issuer; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.springframework.stereotype.Service; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenAttribute; +import de.ozgcloud.token.TokenValidationResult; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class SamlTokenService { + + private final SamlServiceRegistry samlServiceRegistry; + private final ParserPool parserPool; + private final ResponseUnmarshaller responseUnmarshaller; + + public TokenValidationResult validate(String token) { + try { + return buildValidTokenResult(validate(parseToken(token))); + } catch (TokenVerificationException e) { + LOG.debug("Token validation failed", e); + return buildInvalidTokenResult(e); + } + } + + Response parseToken(String token) { + try (var inputStream = new ByteArrayInputStream(token.getBytes(StandardCharsets.UTF_8));) { + var element = parserPool.parse(inputStream).getDocumentElement(); + return (Response) responseUnmarshaller.unmarshall(element); + } catch (IOException | XMLParserException | UnmarshallingException e) { + throw new TokenVerificationException("Cannot read token: " + e.getMessage(), e); + } + } + + Set<TokenAttribute> validate(Response token) { + var tokenIssuer = getTokenIssuer(token); + return getValidationService(tokenIssuer).validate(token); + } + + SamlTokenValidationService getValidationService(String tokenIssuer) { + return samlServiceRegistry.getService(tokenIssuer) + .orElseThrow(() -> new TechnicalException("No validation service found for issuer %s".formatted(tokenIssuer))); + } + + String getTokenIssuer(Response token) { + return Optional.ofNullable(token.getIssuer()).map(Issuer::getValue) + .orElseThrow(() -> new TokenVerificationException("No token issuer found")); + } + + TokenValidationResult buildValidTokenResult(Collection<TokenAttribute> tokenAttributes) { + var resultBuilder = TokenValidationResult.builder().valid(true); + for (var attr : tokenAttributes) { + if (attr.isPostfachId()) { + resultBuilder.postfachId(attr.getValue()); + } else if (attr.isTrustLevel()) { + resultBuilder.trustLevel(attr.getValue()); + } else { + resultBuilder.attribute(attr); + } + } + return resultBuilder.build(); + } + + TokenValidationResult buildInvalidTokenResult(TokenVerificationException exception) { + return TokenValidationResult.builder() + .valid(false) + .errorMesssage(exception.getMessage()) + .build(); + } + +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenUtils.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenUtils.java deleted file mode 100644 index a5d0929fdb414692571aaea9541efd682037ca55..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenUtils.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2024. - * 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 java.io.IOException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.interfaces.RSAPrivateKey; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import org.opensaml.core.xml.XMLObject; -import org.opensaml.saml.common.xml.SAMLConstants; -import org.opensaml.saml.saml2.metadata.EntitiesDescriptor; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.KeyDescriptor; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; -import org.springframework.core.io.Resource; -import org.springframework.security.converter.RsaKeyConverters; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.util.Assert; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class SamlTokenUtils { - - - public static final String NO_CERTIFICATE_LOCATION_SPECIFIED = "No certificate location specified"; - public static final String NO_PRIVATE_KEY_LOCATION_SPECIFIED = "No private key location specified"; - public static final String CERTIFICATE_LOCATION = "Certificate location '"; - public static final String DOES_NOT_EXIST = "' does not exist"; - public static final String PRIVATE_KEY_LOCATION = "Private key location '"; - public static final String MISSING_THE_NECESSARY_IDPSSODESCRIPTOR_ELEMENT = "Metadata response is missing the necessary IDPSSODescriptor element"; - public static final String SAML_ASSERTIONS_VERIFICATION_EMPTY = "Metadata response is missing verification certificates, necessary for verifying SAML assertions"; - - public static Map<String, Boolean> createFeatureMap() { - return Map.of( - FEATURES_EXTERNAL_GENERAL_ENTITIES, Boolean.FALSE, - FEATURES_EXTERNAL_PARAMETER_ENTITIES, Boolean.FALSE, - FEATURES_DISALLOW_DOCTYPE_DECL, Boolean.TRUE, - VALIDATION_SCHEMA_NORMALIZED_VALUE, Boolean.FALSE, - FEATURE_SECURE_PROCESSING, Boolean.TRUE); - } - - public static Saml2X509Credential getDecryptionCredential(Resource key, Resource cert) { - var privateKey = readPrivateKey(key); - var certificate = readCertificateFromResource(cert); - return new Saml2X509Credential(privateKey, certificate, Saml2X509Credential.Saml2X509CredentialType.DECRYPTION); - } - - private static RSAPrivateKey readPrivateKey(Resource location) { - Assert.state(Objects.nonNull(location), NO_PRIVATE_KEY_LOCATION_SPECIFIED); - Assert.state(location.exists(), () -> PRIVATE_KEY_LOCATION + location + DOES_NOT_EXIST); - try (var inputStream = location.getInputStream()) { - return RsaKeyConverters.pkcs8().convert(inputStream); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - } - - private static X509Certificate readCertificateFromResource(Resource location) { - Assert.state(Objects.nonNull(location), NO_CERTIFICATE_LOCATION_SPECIFIED); - Assert.state(location.exists(), () -> CERTIFICATE_LOCATION + location + DOES_NOT_EXIST); - try (var inputStream = location.getInputStream()) { - return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); - } catch (IOException | CertificateException e) { - throw new IllegalArgumentException(e); - } - } - - public static Optional<EntityDescriptor> findEntityDescriptor(XMLObject metadata) { - Optional<EntityDescriptor> descriptor = Optional.empty(); - if (metadata instanceof EntityDescriptor entityDescriptor) { - descriptor = Optional.of(entityDescriptor); - } else if (metadata instanceof EntitiesDescriptor entitiesDescriptor) { - descriptor = entitiesDescriptor.getEntityDescriptors().stream().findFirst(); - } - - return descriptor.filter(entityDescriptor -> entityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null).stream().findFirst(); - } - - public static List<Saml2X509Credential> getVerificationCertificates(EntityDescriptor descriptor) { - var idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); - if (idpssoDescriptor == null) { - throw new Saml2Exception(MISSING_THE_NECESSARY_IDPSSODESCRIPTOR_ELEMENT); - } - List<Saml2X509Credential> verification = new ArrayList<>(); - for (KeyDescriptor keyDescriptor : idpssoDescriptor.getKeyDescriptors()) { - if (keyDescriptor.getUse().equals(UsageType.SIGNING)) { - var certificates = certificates(keyDescriptor); - for (X509Certificate certificate : certificates) { - verification.add(Saml2X509Credential.verification(certificate)); - } - } - } - if (verification.isEmpty()) { - throw new Saml2Exception(SAML_ASSERTIONS_VERIFICATION_EMPTY); - } - - return verification; - } - - private static List<X509Certificate> certificates(KeyDescriptor keyDescriptor) { - try { - return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()); - } catch (CertificateException ex) { - throw new Saml2Exception(ex); - } - } -} 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 new file mode 100644 index 0000000000000000000000000000000000000000..7e47b279a2cd0378fdc61d897743701857a5dc41 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java @@ -0,0 +1,156 @@ +/* + * 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 java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +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.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.encryption.Decrypter; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.SecurityException; +import org.opensaml.xmlsec.encryption.support.DecryptionException; +import org.opensaml.xmlsec.signature.Signature; +import org.opensaml.xmlsec.signature.support.SignatureException; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; + +import de.ozgcloud.token.TokenAttribute; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import lombok.Builder; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; + +@Builder +public class SamlTokenValidationService { + + private final SignatureTrustEngine signatureTrustEngine; + private final Decrypter decrypter; + private final SAMLSignatureProfileValidator profileValidator; + private final TokenValidationProperty tokenValidationProperty; + private final CriteriaSet verificationCriteria; + + public Set<TokenAttribute> validate(Response token) { + validateToken(token); + var tokenAttributes = decryptAttributes(token); + return buildTokenAttributes(tokenAttributes, token); + } + + void validateToken(Response token) { + if (Objects.isNull(token.getSignature())) { + throw new TokenVerificationException("Token signature is missing"); + } + validateSignatureProfile(token.getSignature()); + if (!validateSignature(token.getSignature())) { + throw new TokenVerificationException("Invalid token signature"); + } + } + + void validateSignatureProfile(Signature signature) { + try { + profileValidator.validate(signature); + } catch (SignatureException e) { + throw new TokenVerificationException("Invalid signature profile", e); + } + } + + boolean validateSignature(Signature signature) { + try { + return signatureTrustEngine.validate(signature, verificationCriteria); + } catch (SecurityException e) { + throw new TokenVerificationException("Error on validating signature.", e); + } + } + + public Map<String, String> decryptAttributes(Response token) { + return token.getEncryptedAssertions().stream() + .map(this::decryptAssertion) + .findFirst() + .flatMap(this::getAttributeStatement) + .map(AttributeStatement::getAttributes).stream() + .flatMap(Collection::stream) + .collect(Collectors.toMap(Attribute::getName, this::getAttributeValues)); + } + + Assertion decryptAssertion(EncryptedAssertion assertion) { + try { + return decrypter.decrypt(assertion); + } catch (DecryptionException ex) { + throw new TokenVerificationException("Cannot decrypt token", ex); + } + } + + Optional<AttributeStatement> getAttributeStatement(Assertion assertion) { + return assertion.getStatements().stream() + .filter(AttributeStatement.class::isInstance) + .map(AttributeStatement.class::cast) + .findFirst(); + } + + String getAttributeValues(Attribute attribute) { + return attribute.getAttributeValues().stream().map(this::getAttributeValue).collect(Collectors.joining(";")); + } + + String getAttributeValue(XMLObject value) { + if (Objects.isNull(value)) { + return StringUtils.EMPTY; + } + return switch (value) { + case XSString xsString -> xsString.getValue(); + case XSAny xsAny -> xsAny.getTextContent(); + case XSInteger xsInteger -> String.valueOf(xsInteger.getValue()); + case XSBoolean xsBoolean -> String.valueOf(xsBoolean.getValue()); + default -> value.toString(); + }; + } + + Set<TokenAttribute> buildTokenAttributes(Map<String, String> tokenAttributes, Response token) { + return adjustPostfachIdAttribute(tokenAttributes, token).entrySet().stream().map(this::buildTokenAttribute).collect(Collectors.toSet()); + } + + Map<String, String> adjustPostfachIdAttribute(Map<String, String> tokenAttributes, Response token) { + if (tokenValidationProperty.isUseIdAsPostfachId()) { + tokenAttributes.put(TokenAttribute.POSTFACH_ID_KEY, token.getID()); + } + return tokenAttributes; + } + + TokenAttribute buildTokenAttribute(Map.Entry<String, String> attribute) { + return TokenAttribute.builder().name(attribute.getKey()).value(attribute.getValue()).build(); + } +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java index 9624e34c029338a554fa66713ec7972f96b6e2d1..b1b3de41da2278a7578bd756e1965680829a47c1 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java @@ -72,7 +72,8 @@ class SamlTrustEngineFactory { } CollectionCredentialResolver buildCredentialResolver(TokenValidationProperty entity) { - var credentials = getCertificatesFromMetadata(entity.getMetadata()).map(key -> buildBasicX509Credential(key, entity.getIdpEntityId())) + var credentials = getCertificatesFromMetadata(entity.getMetadata()) + .map(certificate -> buildBasicX509Credential(certificate, entity.getIdpEntityId())) .toList(); if (credentials.isEmpty()) { throw new TechnicalException("Metadata response is missing verification certificates, necessary for verifying SAML assertions"); diff --git a/token-checker-server/src/main/resources/application-local.yml b/token-checker-server/src/main/resources/application-local.yml index 9ee2ec66f2c0caf5880e5d3351fb55a3c314cbef..0f9eaeba57f44763a40dbc070c3b3cc7e77fdc51 100644 --- a/token-checker-server/src/main/resources/application-local.yml +++ b/token-checker-server/src/main/resources/application-local.yml @@ -18,7 +18,7 @@ ozgcloud: key: "classpath:test2-enc.key" certificate: "classpath:test2-enc.crt" metadata: "classpath:metadata/muk-idp-e4k.xml" - userIdAsPostfachId: true + useIdAsPostfachId: true mappings: trustLevel: "ElsterVertrauensniveauAuthentifizierung" server: diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/CheckTokenResultTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/CheckTokenResultTestFactory.java index 338631ac640a535900ed02e45939c95efd622e55..813ce5fe20cacfd5251df713105f142e746df473 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/CheckTokenResultTestFactory.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/CheckTokenResultTestFactory.java @@ -33,7 +33,7 @@ public class CheckTokenResultTestFactory { public static final String POSTFACH_ID = UUID.randomUUID().toString(); public static final String TRUST_LEVEL = "LOW"; - public static final TokenField OTHER_FIELD = TokenAttributeTestFactory.create(); + public static final TokenAttribute OTHER_FIELD = TokenAttributeTestFactory.create(); public static final String ERROR_MESSAGE = TokenVerificationExceptionTestFactory.MESSAGE; static TokenValidationResult createValid() { 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 7a0c83db09fcb6ff2c7ef5e3c4e7ab249659b0af..5e55108e9014a798e8e918de8e32d37a9fddba6c 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 @@ -7,12 +7,12 @@ public class TokenAttributeTestFactory { public static final String NAME = LoremIpsum.getInstance().getWords(1); public static final String VALUE = LoremIpsum.getInstance().getWords(1); - public static TokenField create() { + public static TokenAttribute create() { return createBuilder().build(); } - public static TokenField.TokenFieldBuilder createBuilder() { - return TokenField.builder() + public static TokenAttribute.TokenAttributeBuilder createBuilder() { + return TokenAttribute.builder() .name(NAME) .value(VALUE); } diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceITCase.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceITCase.java index 2498987faa45e1435313b04bcc111255b30cfe6e..5bde3a7cfd77f48cf2b1c05dffd0197b3a5140bb 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceITCase.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceITCase.java @@ -38,10 +38,6 @@ import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.Response; import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.token.saml.Saml2DecryptionService; -import de.ozgcloud.token.saml.Saml2ParseService; -import de.ozgcloud.token.saml.Saml2VerificationService; -import de.ozgcloud.token.saml.SamlSetting; import de.ozgcloud.token.saml.SamlServiceRegistry; import de.ozgcloud.token.saml.SamlTokenTestUtils; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; @@ -97,7 +93,7 @@ class TokenCheckServiceITCase { @Test void shouldGetPostfachHandleFromBayernIdToken() { - var attributes = List.of(new TokenField(POSTFACH_ID_NAME_BAYERN_ID, POSTFACH_ID_BAYERN_ID)); + var attributes = List.of(new TokenAttribute(POSTFACH_ID_NAME_BAYERN_ID, POSTFACH_ID_BAYERN_ID)); when(decryptionService.decryptAttributes(any(), any(SamlSetting.class))).thenReturn( attributes); @@ -108,7 +104,7 @@ class TokenCheckServiceITCase { @Test void shouldGetTrustLevel() { - var attributes = List.of(new TokenField(TRUST_LEVEL_NAME_BAYERN_ID, TRUST_LEVEL)); + var attributes = List.of(new TokenAttribute(TRUST_LEVEL_NAME_BAYERN_ID, TRUST_LEVEL)); when(decryptionService.decryptAttributes(any(), any(SamlSetting.class))).thenReturn( attributes); diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceTest.java deleted file mode 100644 index 66809ad4feb83960b87ff5ba46f3f5f053dd2180..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceTest.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (c) 2024. - * 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 static de.ozgcloud.token.saml.SamlTokenTestUtils.*; -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -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.saml.saml2.core.Issuer; -import org.opensaml.saml.saml2.core.Response; - -import com.thedeanda.lorem.LoremIpsum; - -import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.token.common.errorhandling.TokenVerificationException; -import de.ozgcloud.token.common.errorhandling.TokenVerificationExceptionTestFactory; -import de.ozgcloud.token.saml.Saml2DecryptionService; -import de.ozgcloud.token.saml.Saml2ParseService; -import de.ozgcloud.token.saml.Saml2VerificationService; -import de.ozgcloud.token.saml.SamlSetting; -import de.ozgcloud.token.saml.SamlServiceRegistry; - -class TokenCheckServiceTest { - - @InjectMocks - @Spy - private TokenCheckService service; - - @Mock - private Saml2VerificationService verificationService; - - @Mock - private Saml2DecryptionService decryptionService; - - @Mock - private SamlServiceRegistry samlServiceRegistry; - - @Mock - private Saml2ParseService parseService; - - @Nested - class TestCheckToken { - - private final String token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - - @Test - void shouldCallVerificationService() { - doReturn(CheckTokenResultTestFactory.create()).when(service).getCheckTokenResult(any()); - - checkToken(); - - verify(verificationService).verify(token); - } - - @Nested - class OnValidToken { - - @BeforeEach - void givenValidToken() { - when(verificationService.verify(any())).thenReturn(Collections.emptyList()); - doReturn(CheckTokenResultTestFactory.create()).when(service).getCheckTokenResult(any()); - } - - @Test - void shouldCallGetCheckTokenResult() { - checkToken(); - - verify(service).getCheckTokenResult(token); - } - - @Test - void shouldReturnCheckTokenResult() { - var result = checkToken(); - - assertThat(result).isEqualTo(CheckTokenResultTestFactory.create()); - } - } - - @Nested - class OnInvalidToken { - - @BeforeEach - void givenInvalidToken() { - when(verificationService.verify(any())).thenReturn(List.of(TokenVerificationExceptionTestFactory.createSaml2Error())); - } - - @Test - void shouldThrowTokenVerificationException() { - assertThrows(TokenVerificationException.class, () -> checkToken()); - } - } - - private TokenValidationResult checkToken() { - return service.checkToken(token); - } - } - - @Nested - class TestGetTokenValidationResult { - - @Mock - private SamlSetting samlSetting; - @Mock - private Response response; - @Mock - private Issuer issuer; - - private final String token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - private final TokenValidationResult tokenValidationResult = CheckTokenResultTestFactory.create(); - - @BeforeEach - void mock() { - when(parseService.parse(any())).thenReturn(response); - when(response.getIssuer()).thenReturn(issuer); - when(issuer.getValue()).thenReturn(IDP_ENTITY_ID_BAYERN_ID); - when(samlServiceRegistry.getService(any())).thenReturn(samlSetting); - doReturn(tokenValidationResult).when(service).buildCheckTokenResult(any(), any()); - } - - @Test - void shouldParseToken() { - getCheckTokenResult(); - - verify(parseService).parse(token); - } - - @Test - void shouldGetConfiguration() { - getCheckTokenResult(); - - verify(samlServiceRegistry).getService(IDP_ENTITY_ID_BAYERN_ID); - } - - @Test - void shouldCallBuildCheckTokenResult() { - getCheckTokenResult(); - - verify(service).buildCheckTokenResult(samlSetting, response); - } - - @Test - void shouldReturnCheckTokenResult() { - var result = getCheckTokenResult(); - - assertThat(result).isEqualTo(tokenValidationResult); - } - - private TokenValidationResult getCheckTokenResult() { - return service.getCheckTokenResult(token); - } - } - - @Nested - class TestBuildTokenValidationResult { - - @Mock - private SamlSetting samlSetting; - @Mock - private Response response; - - private final TokenField tokenField = TokenAttributeTestFactory.create(); - private final List<TokenField> decryptedAttributes = List.of(tokenField); - - @BeforeEach - void mock() { - when(decryptionService.decryptAttributes(response, samlSetting)).thenReturn(decryptedAttributes); - doReturn(CheckTokenResultTestFactory.POSTFACH_ID).when(service).getPostfachId(any(), any(), any()); - doReturn(CheckTokenResultTestFactory.TRUST_LEVEL).when(service).findAttributeByKey(any(), any(), any()); - } - - @Test - void shouldDecryptAttributes() { - buildCheckTokenResult(); - - verify(decryptionService).decryptAttributes(response, samlSetting); - } - - @Test - void shouldCallGetPostfachId() { - buildCheckTokenResult(); - - verify(service).getPostfachId(samlSetting, response, decryptedAttributes); - } - - @Test - void shouldCallFindAttributeKey() { - buildCheckTokenResult(); - - verify(service).findAttributeByKey(TokenCheckService.TRUST_LEVEL_KEY, decryptedAttributes, samlSetting); - } - - @Test - void shouldReturnCheckTokenResult() { - var result = buildCheckTokenResult(); - - assertThat(result).isEqualTo(CheckTokenResultTestFactory.create()); - } - - private TokenValidationResult buildCheckTokenResult() { - return service.buildCheckTokenResult(samlSetting, response); - } - } - - @Nested - class TestGetPostfachId { - - @Mock - private SamlSetting samlSetting; - @Mock - private Response response; - - private final TokenField tokenField = TokenAttributeTestFactory.create(); - private final List<TokenField> decryptedAttributes = List.of(tokenField); - - @Test - void shouldCheckIsIdIsPostfachId() { - getPostfachId(); - - verify(samlSetting).isIdIsPostfachId(); - } - - @Nested - class OnIdIsPostfachId { - @BeforeEach - void mock() { - when(samlSetting.isIdIsPostfachId()).thenReturn(true); - when(response.getID()).thenReturn(CheckTokenResultTestFactory.POSTFACH_ID); - } - - @Test - void shouldReturnResponseId() { - var postfachId = getPostfachId(); - - assertThat(postfachId).isEqualTo(CheckTokenResultTestFactory.POSTFACH_ID); - } - } - - @Nested - class OnIdIsNotPostfachId { - @BeforeEach - void mock() { - when(samlSetting.isIdIsPostfachId()).thenReturn(false); - } - - @Test - void shouldCallFindAttributeByKey() { - getPostfachId(); - - verify(service).findAttributeByKey(TokenCheckService.POSTFACH_ID_KEY, decryptedAttributes, samlSetting); - } - - @Test - void shouldReturnAttributeValue() { - var id = UUID.randomUUID().toString(); - doReturn(id).when(service).findAttributeByKey(any(), any(), any()); - - var postfachId = getPostfachId(); - - assertThat(postfachId).isEqualTo(id); - } - } - - private String getPostfachId() { - return service.getPostfachId(samlSetting, response, decryptedAttributes); - } - } - - @Nested - class TestFindAttributeByKey { - - @Mock - private SamlSetting samlSetting; - - private final String existingKey = LoremIpsum.getInstance().getWords(1); - private final String nonExistingKey = LoremIpsum.getInstance().getWords(1); - private final String attributeName = LoremIpsum.getInstance().getWords(1); - private final String nonExistingName = LoremIpsum.getInstance().getWords(1); - private final String attributeValue = LoremIpsum.getInstance().getWords(1); - private final Map<String, String> mappings = Map.of(existingKey, attributeName, nonExistingKey, nonExistingName); - - private final TokenField tokenField1 = TokenAttributeTestFactory.create(); - private final TokenField tokenField2 = TokenAttributeTestFactory.createBuilder() - .name(attributeName) - .value(attributeValue) - .build(); - private final List<TokenField> attributes = List.of(tokenField1, tokenField2); - - @BeforeEach - void setUp() { - when(samlSetting.getMappings()).thenReturn(mappings); - } - - @Test - void shouldReturnAttributeValue() { - var value = service.findAttributeByKey(existingKey, attributes, samlSetting); - - assertThat(value).isEqualTo(attributeValue); - } - - @Test - void shouldReturnEmptyString() { - var value = service.findAttributeByKey(nonExistingKey, attributes, samlSetting); - - assertThat(value).isEmpty(); - } - - } -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckTestConfiguration.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckTestConfiguration.java index 306dc8250e85091383e5ea59f6c2fac70093034e..0545a4faa0e91564ee4b8b0a5449272466f483f7 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckTestConfiguration.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckTestConfiguration.java @@ -27,7 +27,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import de.ozgcloud.token.saml.SamlServiceRegistry; -import de.ozgcloud.token.saml.SamlTokenUtils; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.ParserPool; diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..639d2b5e6f26fcbbc0ab7e75fcbcb18e66f0199c --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenServiceTest.java @@ -0,0 +1,48 @@ +/* + * 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 org.junit.jupiter.api.Nested; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.thedeanda.lorem.LoremIpsum; + +class SamlTokenServiceTest { + + private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7); + + @Spy + @InjectMocks + private SamlTokenService service; + + @Mock + private SamlServiceRegistry samlServiceRegistry; + + @Nested + class TestValidate { + + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..09e569c096c8bd0e1f4b10a4b024b13922fa3fe9 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java @@ -0,0 +1,136 @@ +/* + * 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.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.stream.Stream; + +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.config.XMLObjectProviderRegistry; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.springframework.core.io.Resource; +import org.springframework.security.saml2.core.Saml2X509Credential; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import net.shibboleth.utilities.java.support.xml.ParserPool; + +class SamlTrustEngineFactoryTest { + + @Spy + @InjectMocks + private SamlTrustEngineFactory factory; + + @Mock + private XMLObjectProviderRegistry registry; + @Mock + private ParserPool parserPool; + + @Nested + class TestBuildSamlTrustEngine { + + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private CollectionCredentialResolver credentialResolver; + + @BeforeEach + void init() { + doReturn(credentialResolver).when(factory).buildCredentialResolver(any()); + } + + @Test + void shouldCallBuildCredentialResolver() { + factory.buildSamlTrustEngine(tokenValidationProperty); + + verify(factory).buildCredentialResolver(tokenValidationProperty); + } + + @Test + void shouldSetCredentialResolver() { + var result = factory.buildSamlTrustEngine(tokenValidationProperty); + + assertThat(result).isInstanceOf(ExplicitKeySignatureTrustEngine.class).extracting("credentialResolver").isEqualTo(credentialResolver); + } + + @Test + void shouldSetKeyInfoResolver() { + var result = factory.buildSamlTrustEngine(tokenValidationProperty); + + assertThat(result).isInstanceOf(ExplicitKeySignatureTrustEngine.class).extracting("keyInfoResolver").isNotNull(); + } + } + + @Nested + class TestBuildCredentialResolver { + + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private Saml2X509Credential credential; + @Mock + private Resource metadata; + + @BeforeEach + void init() { + when(tokenValidationProperty.getMetadata()).thenReturn(metadata); + } + + @Test + void shouldCallGetCertificatesFromMetadata() { + when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.of(credential)); + + factory.buildCredentialResolver(tokenValidationProperty); + + verify(factory).getCertificatesFromMetadata(metadata); + } + + @Test + void shouldThrowExceptionWhenNoCertificates() { + when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.empty()); + + assertThatThrownBy(() -> factory.buildCredentialResolver(tokenValidationProperty)).isInstanceOf(TechnicalException.class) + .hasMessage("Metadata response is missing verification certificates, necessary for verifying SAML assertions"); + } + + @Test + void shouldBuildCredentialResolver() { + when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.of("key")); + doReturn(new CollectionCredentialResolver(null)).when(factory).buildCredentialResolver(any()); + + var result = factory.buildCredentialResolver(tokenValidationProperty); + + assertThat(result).isNotNull(); + } + } +} \ No newline at end of file