diff --git a/Jenkinsfile b/Jenkinsfile index 90e402d1e0d747b339687dbf13dfbe15c56b26f6..1a3ba53bf547691550d79e07a0a2a71090376209 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -78,7 +78,6 @@ pipeline { } configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { sh 'mvn -s $MAVEN_SETTINGS -DskipTests deploy -Dmaven.wagon.http.retryHandler.count=3' - sh "mvn -s $MAVEN_SETTINGS versions:revert" } } } @@ -88,10 +87,11 @@ pipeline { FAILED_STAGE=env.STAGE_NAME } configFileProvider([configFile(fileId: 'maven-settings', variable: 'MAVEN_SETTINGS')]) { - dir('token-checker-server'){ - sh 'mvn --no-transfer-progress -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3' - } - + dir('token-checker-server'){ + sh 'mvn --no-transfer-progress -s $MAVEN_SETTINGS spring-boot:build-image -DskipTests -Dmaven.wagon.http.retryHandler.count=3' + sh "mvn -s $MAVEN_SETTINGS versions:revert" + } + } } } diff --git a/README.md b/README.md index 482ef604b57d20f18e9a02200fc6820e62f7e27c..b133efcca4030640a86f76b390ada99d2cb0b31c 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,5 @@ Zentraler Dienst um (Saml-)Token auf Gültigkeit zu prüfen. Unterstützt werden * otherFields: Repeated: * name: String * value: String - * checkError - * message: String + * checkError + * checkErrors: GrpcError \ No newline at end of file diff --git a/token-checker-interface/pom.xml b/token-checker-interface/pom.xml index 1edb7ebe4f496453be29390b549bc273839eb8fc..a24fe2020b694d937b2faaf9554ab3de038eeb68 100644 --- a/token-checker-interface/pom.xml +++ b/token-checker-interface/pom.xml @@ -31,7 +31,7 @@ <parent> <groupId>de.ozgcloud.common</groupId> <artifactId>ozgcloud-common-dependencies</artifactId> - <version>4.3.1</version> + <version>4.7.0</version> <relativePath/> </parent> diff --git a/token-checker-interface/src/main/protobuf/tokencheck.model.proto b/token-checker-interface/src/main/protobuf/tokencheck.model.proto index b24b8089fa309eabceb479f5846501ebe2ab2480..a2edda6189d7b7736fe5c44ffd6d5e87abf384fb 100644 --- a/token-checker-interface/src/main/protobuf/tokencheck.model.proto +++ b/token-checker-interface/src/main/protobuf/tokencheck.model.proto @@ -6,17 +6,33 @@ option java_multiple_files = true; option java_package = "de.ozgcloud.token"; option java_outer_classname = "TokenCheckModelProto"; -message GrpcTokenAttribute { +message GrpcCheckTokenRequest { + string token = 1; +} + +message GrpcCheckTokenResponse { + bool tokenValid = 1; + oneof checkTokenResult { + GrpcTokenAttributes tokenAttributes = 2; + GrpcCheckErrors checkErrors = 3; + } +} + +message GrpcTokenAttributes { + string postfachId = 1; + string trustLevel = 2; + repeated GrpcOtherField otherFields = 3; +} + +message GrpcOtherField { string name = 1; string value = 2; } -message GrpcTokenCheckRequest { - string token = 1; +message GrpcCheckErrors { + repeated GrpcCheckError checkError = 1; } -message GrpcTokenCheckResponse { - string postkorbHandle = 1; - string trustLevel = 2; - repeated GrpcTokenAttribute otherFields = 3; -} \ No newline at end of file +message GrpcCheckError { + string message = 1; +} diff --git a/token-checker-interface/src/main/protobuf/tokencheck.proto b/token-checker-interface/src/main/protobuf/tokencheck.proto index ee9cb45090330e17cfbc1c6db0e9d3c3b0e12a47..3ae872114ebe89136bc1e543841649fc2297fa05 100644 --- a/token-checker-interface/src/main/protobuf/tokencheck.proto +++ b/token-checker-interface/src/main/protobuf/tokencheck.proto @@ -9,6 +9,6 @@ option java_package = "de.ozgcloud.token"; option java_outer_classname = "TokenCheckProto"; service TokenCheckService { - rpc CheckToken(GrpcTokenCheckRequest) returns (GrpcTokenCheckResponse) { + rpc CheckToken(GrpcCheckTokenRequest) returns (GrpcCheckTokenResponse) { } } \ No newline at end of file diff --git a/token-checker-server/pom.xml b/token-checker-server/pom.xml index b36b6be51a4562270325d238bf751304bd65aceb..92b8d055956dd90c4ab79f6f8ecb329b8c216b4d 100644 --- a/token-checker-server/pom.xml +++ b/token-checker-server/pom.xml @@ -32,7 +32,7 @@ <parent> <groupId>de.ozgcloud.common</groupId> <artifactId>ozgcloud-common-parent</artifactId> - <version>4.5.0</version> + <version>4.7.0</version> <relativePath/> </parent> @@ -64,7 +64,7 @@ <dependency> <groupId>de.ozgcloud.token</groupId> <artifactId>token-checker-interface</artifactId> - <version>0.1.0-SNAPSHOT</version> + <version>${project.version}</version> </dependency> <dependency> diff --git a/token-checker-server/src/main/helm/templates/configmap_bindings_type.yaml b/token-checker-server/src/main/helm/templates/configmap_bindings_type.yaml index d03cc0e61712bbf4ac6fbad0f9c67b9ecadcbafc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/main/helm/templates/configmap_bindings_type.yaml +++ b/token-checker-server/src/main/helm/templates/configmap_bindings_type.yaml @@ -1,32 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: token-checker-server-bindings-type - namespace: {{ include "app.namespace" . }} -data: - type: | - ca-certificates \ No newline at end of file diff --git a/token-checker-server/src/main/helm/templates/deployment.yaml b/token-checker-server/src/main/helm/templates/deployment.yaml index 9a1d1b9fd3f9c4b7e4754c5e4e534b67b7af0296..c6f4b6f6b03b091b53d9b05c6df4dab0c86662f5 100644 --- a/token-checker-server/src/main/helm/templates/deployment.yaml +++ b/token-checker-server/src/main/helm/templates/deployment.yaml @@ -59,8 +59,6 @@ spec: app.kubernetes.io/name: {{ .Release.Name }} containers: - env: - - name: SERVICE_BINDING_ROOT - value: "/bindings" - name: spring_profiles_active value: {{ include "app.envSpringProfiles" . }} @@ -72,10 +70,14 @@ spec: value: file:///keystore/enc.crt - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_METADATA value: file:///metadata/muk-idp-infra.xml - - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_USE-ID-AS-POSTKORB-HANDLE - value: {{ quote (index ((.Values.ozgcloud).tokenChecker).entities 0).useIdAsPostkorbHandle | default "\"true\""}} + - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_USE-ID-AS-POSTFACH-ID + value: {{ quote (index ((.Values.ozgcloud).tokenChecker).entities 0).useIdAsPostfachId | default "\"true\""}} - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_MAPPINGS_TRUST-LEVEL value: {{ required "at least one ozgcloud.token.check.entities.mappings trustlevel must be set" (index ((.Values.ozgcloud).tokenChecker).entities 0).mappings.trustLevel }} + {{- if eq (index ((.Values.ozgcloud).tokenChecker).entities 0).useIdAsPostfachId false }} + - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_MAPPINGS_POSTFACH-ID + value: {{ required "at least one ozgcloud.token.check.entities.mappings postfachId must be set" (index ((.Values.ozgcloud).tokenChecker).entities 0).mappings.postfachId }} + {{- end }} {{- with include "app.getCustomList" . }} {{ . | indent 10 }} @@ -134,10 +136,6 @@ spec: terminationMessagePolicy: File tty: true volumeMounts: - - name: bindings - mountPath: "/bindings/ca-certificates/type" - subPath: type - readOnly: true - name: saml-mount mountPath: "/keystore/enc.crt" subPath: enc.crt @@ -152,9 +150,6 @@ spec: readOnly: true volumes: - - name: bindings - configMap: - name: token-checker-server-bindings-type - name: saml-mount secret: secretName: {{ required ".Values.samlRegistrationSecretName must be set" .Values.samlRegistrationSecretName }} diff --git a/token-checker-server/src/main/helm/templates/service_account.yaml b/token-checker-server/src/main/helm/templates/service_account.yaml new file mode 100644 index 0000000000000000000000000000000000000000..08fcd4055f5ea66a81b42c228a23ded16c10ae62 --- /dev/null +++ b/token-checker-server/src/main/helm/templates/service_account.yaml @@ -0,0 +1,7 @@ +{{- if (.Values.serviceAccount).create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "app.serviceAccountName" . }} + namespace: {{ include "app.namespace" . }} +{{- end }} \ No newline at end of file diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/GrpcTokenCheckService.java b/token-checker-server/src/main/java/de/ozgcloud/token/GrpcTokenCheckService.java index 99d36b9f2f5b09a79dfa4c04a4f503aa5b836034..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/GrpcTokenCheckService.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/GrpcTokenCheckService.java @@ -1,50 +0,0 @@ -/* - * 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 io.grpc.stub.StreamObserver; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import net.devh.boot.grpc.server.service.GrpcService; - -@Log4j2 -@GrpcService -@RequiredArgsConstructor -public class GrpcTokenCheckService extends TokenCheckServiceGrpc.TokenCheckServiceImplBase { - private final TokenCheckService tokenCheckerService; - private final TokenCheckMapper mapper; - - @Override - public void checkToken(GrpcTokenCheckRequest request, StreamObserver<GrpcTokenCheckResponse> responseStreamObserver) { - try { - var result = tokenCheckerService.checkToken(request.getToken()); - - responseStreamObserver.onNext(mapper.toGrpcTokenResponse(result)); - responseStreamObserver.onCompleted(); - } catch (Exception e) { - LOG.error("Error checking token.", e); - responseStreamObserver.onError(e); - } - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java index 096eafcd00e33ba9942f87de7c788cd7e3d8cfe5..4e921a71690d7f4a5eb587c8900f96ffbd82c806 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttribute.java @@ -24,7 +24,14 @@ package de.ozgcloud.token; import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; @Builder -public record TokenAttribute(String name, String value) { -} +@Getter +public class TokenAttribute { + + private final String name; + private final String value; + +} \ No newline at end of file diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenVerificationException.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttributes.java similarity index 74% rename from token-checker-server/src/main/java/de/ozgcloud/token/TokenVerificationException.java rename to token-checker-server/src/main/java/de/ozgcloud/token/TokenAttributes.java index 82580a0b8ec3e389755726f5a500063e7521c281..6370db20f6a07c9619afc56b1bf92dadf2b8fc9e 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenVerificationException.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenAttributes.java @@ -25,17 +25,20 @@ package de.ozgcloud.token; import java.util.List; -import org.springframework.security.saml2.core.Saml2Error; - -import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.Builder; import lombok.Getter; +import lombok.Singular; +@Builder @Getter -public class TokenVerificationException extends TechnicalException { - private final List<Saml2Error> errorList; +public class TokenAttributes { + + public static final String POSTFACH_ID_KEY = "postfachId"; + public static final String TRUST_LEVEL_KEY = "trustLevel"; + + private final String postfachId; + private final String trustLevel; + @Singular + private final List<TokenAttribute> otherAttributes; - public TokenVerificationException(final String msg, final List<Saml2Error> errorList) { - super(msg); - this.errorList = errorList; - } } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckApplication.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckApplication.java index 40887bc9b8cecb28e1d85ae45afc65420465effe..cf92357a52994e837531002891a7d9461c8d3e7e 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckApplication.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckApplication.java @@ -23,25 +23,16 @@ */ package de.ozgcloud.token; -import java.util.HashMap; import java.util.TimeZone; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.EnableAspectJAutoProxy; -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; - @SpringBootApplication(scanBasePackages = { "de.ozgcloud" }) @ConfigurationPropertiesScan("de.ozgcloud.token") @EnableAspectJAutoProxy(proxyTargetClass = true) -@EnableConfigurationProperties(TokenCheckProperties.class) public class TokenCheckApplication { public static void main(String[] args) { @@ -49,15 +40,4 @@ public class TokenCheckApplication { SpringApplication.run(TokenCheckApplication.class, args); } - @Bean - public ParserPool parserPool() throws ComponentInitializationException { - var localParserPool = new BasicParserPool(); - - final var features = SamlTokenUtils.createFeatureMap(); - localParserPool.setBuilderFeatures(features); - localParserPool.setBuilderAttributes(new HashMap<>()); - localParserPool.initialize(); - - return localParserPool; - } } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckConfiguration.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckConfiguration.java deleted file mode 100644 index 21ff51b9289e14e7ba4cae74c1cbb6bfec0583a2..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckConfiguration.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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 static de.ozgcloud.token.saml.SamlTokenUtils.*; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import jakarta.annotation.PostConstruct; - -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.config.InitializationService; -import org.opensaml.core.criterion.EntityIdCriterion; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.saml.criterion.ProtocolCriterion; -import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; -import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; -import org.opensaml.security.credential.Credential; -import org.opensaml.security.credential.CredentialResolver; -import org.opensaml.security.credential.CredentialSupport; -import org.opensaml.security.credential.UsageType; -import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; -import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; -import org.opensaml.security.credential.impl.CollectionCredentialResolver; -import org.opensaml.security.criteria.UsageCriterion; -import org.opensaml.security.x509.BasicX509Credential; -import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; -import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; -import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; -import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; -import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; -import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.Resource; -import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2X509Credential; -import org.springframework.stereotype.Component; - -import de.ozgcloud.common.errorhandling.TechnicalException; -import de.ozgcloud.token.saml.ConfigurationEntity; -import de.ozgcloud.token.saml.SamlConfiguration; -import de.ozgcloud.token.saml.SamlConfigurationRegistry; -import de.ozgcloud.token.saml.SamlTokenUtils; -import lombok.RequiredArgsConstructor; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import net.shibboleth.utilities.java.support.xml.XMLParserException; - -@Component -@Configuration -@RequiredArgsConstructor -public class TokenCheckConfiguration { - private XMLObjectProviderRegistry registry; - private final TokenCheckProperties tokenCheckerProperties; - private final ParserPool parserPool; - private final SamlConfigurationRegistry samlConfigurationRegistry; - - private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( - Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), - new SimpleRetrievalMethodEncryptedKeyResolver())); - - @PostConstruct - public void initOpenSAML() { - try { - registry = new XMLObjectProviderRegistry(); - ConfigurationService.register(XMLObjectProviderRegistry.class, registry); - - registry.setParserPool(parserPool); - InitializationService.initialize(); - - tokenCheckerProperties.getEntities().forEach(this::initSamlConfiguration); - } catch (Exception e) { - throw new TechnicalException("Initialization failed", e); - } - } - - private void initSamlConfiguration(final ConfigurationEntity entity) { - var trustEngine = initTrustEngine(entity.getMetadata(), entity.getIdpEntityId()); - var verificationCriteria = getVerificationCriteria(entity.getIdpEntityId()); - var decrypter = initDecrypter(entity); - - var samlConfiguration = new SamlConfiguration(trustEngine, verificationCriteria, decrypter, entity.getMappings(), - entity.getUseIdAsPostkorbHandle()); - - samlConfigurationRegistry.addConfiguration(entity.getIdpEntityId(), samlConfiguration); - } - - private SignatureTrustEngine initTrustEngine(Resource metadata, String idpEntityId) { - Set<Credential> credentials = new HashSet<>(); - Collection<Saml2X509Credential> keys = getCertificatesFromMetadata(metadata); - - for (Saml2X509Credential key : keys) { - var cred = new BasicX509Credential(key.getCertificate()); - cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(idpEntityId); - credentials.add(cred); - } - - CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - return new ExplicitKeySignatureTrustEngine(credentialsResolver, - DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); - } - - private CriteriaSet getVerificationCriteria(String idpEntityId) { - var criteria = new CriteriaSet(); - criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(idpEntityId))); - criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion("urn:oasis:names:tc:SAML:2.0:protocol"))); - criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); - return criteria; - } - - private List<Saml2X509Credential> getCertificatesFromMetadata(Resource metadataResource) { - try (var metadata = metadataResource.getInputStream()) { - var xmlObject = xmlObject(metadata); - var descriptorOptional = findEntityDescriptor(xmlObject); - if (descriptorOptional.isPresent()) { - return SamlTokenUtils.getVerificationCertificates(descriptorOptional.get()); - } - } catch (IOException e) { - throw new Saml2Exception("Error reading idp metadata.", e); - } catch (XMLParserException e) { - throw new Saml2Exception("Error initializing parser pool.", e); - } - - throw new Saml2Exception("No IDPSSO Descriptors found"); - } - - XMLObject xmlObject(InputStream inputStream) throws XMLParserException { - var document = parserPool.parse(inputStream); - var element = document.getDocumentElement(); - var unmarshaller = registry.getUnmarshallerFactory().getUnmarshaller(element); - if (unmarshaller == null) { - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - try { - return unmarshaller.unmarshall(element); - } catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private Decrypter initDecrypter(ConfigurationEntity entity) { - var credentials = new ArrayList<Credential>(); - var decryptionX509Credential = SamlTokenUtils.getDecryptionCredential(entity.getKey(), entity.getCertificate()); - - var cred = CredentialSupport.getSimpleCredential(decryptionX509Credential.getCertificate(), decryptionX509Credential.getPrivateKey()); - credentials.add(cred); - - var resolver = new CollectionKeyInfoCredentialResolver(credentials); - var setupDecrypter = new Decrypter(null, resolver, encryptedKeyResolver); - setupDecrypter.setRootInNewDocument(true); - - return setupDecrypter; - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckGrpcService.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckGrpcService.java new file mode 100644 index 0000000000000000000000000000000000000000..bfc048400caf019595943e0a24cc1da8ca5376f9 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckGrpcService.java @@ -0,0 +1,60 @@ +/* + * 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 de.ozgcloud.token.saml.SamlTokenService; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.devh.boot.grpc.server.service.GrpcService; + +@Log4j2 +@GrpcService +@RequiredArgsConstructor +public class TokenCheckGrpcService extends TokenCheckServiceGrpc.TokenCheckServiceImplBase { + + private final SamlTokenService samlTokenService; + private final TokenValidationResultMapper tokenCheckMapper; + + @Override + public void checkToken(GrpcCheckTokenRequest request, StreamObserver<GrpcCheckTokenResponse> responseStreamObserver) { + var result = samlTokenService.validate(request.getToken()); + responseStreamObserver.onNext(buildCheckResponse(result)); + responseStreamObserver.onCompleted(); + } + + GrpcCheckTokenResponse buildCheckResponse(TokenValidationResult result) { + return result.isValid() ? buildValidCheckTokenResponse(result) : buildInvalidCheckTokenResponse(result); + } + + GrpcCheckTokenResponse buildValidCheckTokenResponse(TokenValidationResult result) { + return GrpcCheckTokenResponse.newBuilder() + .setTokenValid(true) + .setTokenAttributes(tokenCheckMapper.toTokenAttributes(result)).build(); + } + + GrpcCheckTokenResponse buildInvalidCheckTokenResponse(TokenValidationResult result) { + return GrpcCheckTokenResponse.newBuilder() + .setTokenValid(false) + .setCheckErrors(tokenCheckMapper.toCheckErrors(result)) + .build(); + } +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckProperties.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckProperties.java deleted file mode 100644 index 0dd9952392fb9b885f4a0358fee1b4ced7adbada..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckProperties.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.List; - -import jakarta.validation.constraints.NotEmpty; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import de.ozgcloud.token.saml.ConfigurationEntity; -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -@ConfigurationProperties(prefix = TokenCheckProperties.PREFIX) -public class TokenCheckProperties { - static final String PREFIX = "ozgcloud.token.check"; - /** - * List of entities. A ConfigurationEntity contains the necessary information for verifying and decrypting saml tokens. - */ - @NotEmpty - private List<ConfigurationEntity> entities; -} 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 6d6acdca634c698c7ea8c2bf5bbb5b6f45a69c52..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.List; - -import org.opensaml.saml.saml2.core.Response; -import org.springframework.stereotype.Service; - -import de.ozgcloud.token.saml.Saml2DecryptionService; -import de.ozgcloud.token.saml.Saml2ParseService; -import de.ozgcloud.token.saml.Saml2VerificationService; -import de.ozgcloud.token.saml.SamlConfiguration; -import de.ozgcloud.token.saml.SamlConfigurationRegistry; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -public class TokenCheckService { - public static final String POSTKORB_HANDLE_KEY = "postkorbHandle"; - public static final String TRUST_LEVEL_KEY = "trustLevel"; - - private final SamlConfigurationRegistry samlConfigurationRegistry; - private final Saml2DecryptionService decryptionService; - private final Saml2ParseService parseService; - private final Saml2VerificationService verificationService; - - public TokenCheckResult checkToken(final String token) { - var errors = verificationService.verify(token); - if (errors.isEmpty()) { - return getTokenCheckResult(token); - } - - throw new TokenVerificationException("Errors occurred while checking token", errors); - } - - TokenCheckResult getTokenCheckResult(final String token) { - var response = parseService.parse(token); - var samlConfiguration = samlConfigurationRegistry.getConfiguration(response.getIssuer().getValue()); - - var decryptedAttributes = decryptionService.decryptAttributes(response, samlConfiguration); - - String postkorbHandle = getPostkorbHandle(samlConfiguration, response, decryptedAttributes); - - return TokenCheckResult.builder().attributes(decryptedAttributes).postkorbHandle(postkorbHandle) - .trustLevel(findAttributeByKey(TRUST_LEVEL_KEY, decryptedAttributes, samlConfiguration)).build(); - } - - private String getPostkorbHandle(final SamlConfiguration samlConfiguration, final Response response, - final List<TokenAttribute> decryptedAttributes) { - String postkorbHandle; - if (samlConfiguration.idIsPostKorbMapping()) { - postkorbHandle = response.getID(); - } else { - postkorbHandle = findAttributeByKey(POSTKORB_HANDLE_KEY, decryptedAttributes, samlConfiguration); - } - return postkorbHandle; - } - - private String findAttributeByKey(String key, List<TokenAttribute> attributes, SamlConfiguration samlConfiguration) { - var name = samlConfiguration.mappings().get(key); - - return attributes.stream().filter(attribute -> attribute.name().equals(name)) - .findFirst().map(TokenAttribute::value).orElse(""); - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckerConfiguration.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckerConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..8d3ae5ad512f5951db840ac589d814c641b8b64e --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckerConfiguration.java @@ -0,0 +1,72 @@ +/* + * 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.Map; + +import org.opensaml.core.config.ConfigurationService; +import org.opensaml.core.config.InitializationException; +import org.opensaml.core.config.InitializationService; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.xml.BasicParserPool; +import net.shibboleth.utilities.java.support.xml.ParserPool; + +@Configuration +public class TokenCheckerConfiguration { + + public static final String FEATURES_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; + public static final String FEATURES_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; + public static final String FEATURES_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; + public static final String VALIDATION_SCHEMA_NORMALIZED_VALUE = "http://apache.org/xml/features/validation/schema/normalized-value"; + public static final String FEATURE_SECURE_PROCESSING = "http://javax.xml.XMLConstants/feature/secure-processing"; + + @Bean + ParserPool parserPool() throws ComponentInitializationException { + var localParserPool = new BasicParserPool(); + localParserPool.setBuilderFeatures(createFeatureMap()); + localParserPool.initialize(); + + return localParserPool; + } + + 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); + } + + @Bean + XMLObjectProviderRegistry xmlObjectProviderRegistry(ParserPool parserPool) throws InitializationException { + var registry = new XMLObjectProviderRegistry(); + registry.setParserPool(parserPool); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + InitializationService.initialize(); + return registry; + } + +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..f82afaf80bde3dd7b2beb14e9701360b7713a6ed --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationProperties.java @@ -0,0 +1,92 @@ +/* + * 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 java.util.Map; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Setter +@Getter +@Configuration +@ConfigurationProperties(prefix = TokenValidationProperties.PREFIX) +public class TokenValidationProperties { + + static final String PREFIX = "ozgcloud.token.check"; + /** + * List of entities. A ConfigurationEntity contains the necessary information for verifying and decrypting saml tokens. + */ + @NotEmpty + @Valid + private List<TokenValidationProperty> entities; + + @Getter + @Setter + @ToString + public static class TokenValidationProperty { + + /** + * The id of the Identity Provider, this is also the issuer value. + */ + @NotEmpty + private String idpEntityId; + + /** + * The encryption key + */ + @NotEmpty + private Resource key; + + /** + * The encryption certificate + */ + @NotEmpty + private Resource certificate; + + /** + * The url or the actual SAML Metadata file received from the idp + */ + @NotEmpty + private Resource metadata; + + /** + * Use the user id as Postkorbhandle. For Muk + */ + private boolean useIdAsPostfachId = false; + + /** + * The mappings define the names of the attributes in the SamlResponse that correspond to these keys + */ + @NotNull + private Map<String, String> mappings; + } +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckMapper.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java similarity index 70% rename from token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckMapper.java rename to token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java index 096f996a6175a20123627f21b498224e121ef228..5e1fc87756cb9a788d75a34e2343b6c0bb5b1324 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckMapper.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResult.java @@ -25,14 +25,18 @@ package de.ozgcloud.token; import java.util.List; -import org.mapstruct.CollectionMappingStrategy; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; +import de.ozgcloud.token.common.errorhandling.ValidationError; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; -@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) -interface TokenCheckMapper { - @Mapping(source = "attributes", target = "otherFieldsList") - GrpcTokenCheckResponse toGrpcTokenResponse(TokenCheckResult result); +@Builder +@Getter +public class TokenValidationResult { + + private final boolean valid; + private final TokenAttributes attributes; + @Singular + private final List<ValidationError> validationErrors; - List<GrpcTokenAttribute> toGrpcTokenAttributeList(List<TokenAttribute> attributes); } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResultMapper.java b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResultMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..8fcc946550056bcc08d1f2d7b1075da4c4630ff2 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/TokenValidationResultMapper.java @@ -0,0 +1,77 @@ +/* + * 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 org.mapstruct.CollectionMappingStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValueCheckStrategy; +import org.mapstruct.NullValueMappingStrategy; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.ReportingPolicy; + +import de.ozgcloud.token.common.errorhandling.ValidationError; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.WARN, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, // + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, + nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT) +interface TokenValidationResultMapper { + + @Mapping(target = "unknownFields", ignore = true) + @Mapping(target = "trustLevelBytes", ignore = true) + @Mapping(target = "removeOtherFields", ignore = true) + @Mapping(target = "postfachIdBytes", ignore = true) + @Mapping(target = "otherFieldsOrBuilderList", ignore = true) + @Mapping(target = "otherFieldsBuilderList", ignore = true) + @Mapping(target = "mergeUnknownFields", ignore = true) + @Mapping(target = "mergeFrom", ignore = true) + @Mapping(target = "defaultInstanceForType", ignore = true) + @Mapping(target = "clearOneof", ignore = true) + @Mapping(target = "clearField", ignore = true) + @Mapping(target = "allFields", ignore = true) + @Mapping(target = "postfachId", source = "attributes.postfachId") + @Mapping(target = "trustLevel", source = "attributes.trustLevel") + @Mapping(target = "otherFieldsList", source = "attributes.otherAttributes") + GrpcTokenAttributes toTokenAttributes(TokenValidationResult validationResult); + + @Mapping(target = "unknownFields", ignore = true) + @Mapping(target = "removeCheckError", ignore = true) + @Mapping(target = "mergeUnknownFields", ignore = true) + @Mapping(target = "mergeFrom", ignore = true) + @Mapping(target = "defaultInstanceForType", ignore = true) + @Mapping(target = "clearOneof", ignore = true) + @Mapping(target = "clearField", ignore = true) + @Mapping(target = "checkErrorOrBuilderList", ignore = true) + @Mapping(target = "checkErrorBuilderList", ignore = true) + @Mapping(target = "allFields", ignore = true) + @Mapping(target = "checkErrorList", source = "validationErrors") + GrpcCheckErrors toCheckErrors(TokenValidationResult validationResult); + + @Mapping(target = "unknownFields", ignore = true) + @Mapping(target = "messageBytes", ignore = true) + @Mapping(target = "mergeUnknownFields", ignore = true) + @Mapping(target = "mergeFrom", ignore = true) + @Mapping(target = "defaultInstanceForType", ignore = true) + @Mapping(target = "clearOneof", ignore = true) + @Mapping(target = "clearField", ignore = true) + @Mapping(target = "allFields", ignore = true) + GrpcCheckError toCheckError(ValidationError validationError); +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..b89d4feefcaebb831023c25157d6bf0e55e23d20 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptor.java @@ -0,0 +1,95 @@ +/* + * 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.common; + +import java.util.UUID; + +import org.apache.logging.log4j.CloseableThreadContext; + +import de.ozgcloud.common.grpc.GrpcUtil; +import io.grpc.ForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import lombok.RequiredArgsConstructor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +@GrpcGlobalServerInterceptor +@RequiredArgsConstructor +class CallContextGrpcServerInterceptor implements ServerInterceptor { + + @Override + public <A, B> Listener<A> interceptCall(ServerCall<A, B> call, Metadata headers, ServerCallHandler<A, B> next) { + return new LogContextSettingListener<>(next.startCall(call, headers), headers); + } + + class LogContextSettingListener<A> extends ForwardingServerCallListener.SimpleForwardingServerCallListener<A> { + + private final String requestId; + + public LogContextSettingListener(Listener<A> delegate, Metadata headers) { + super(delegate); + this.requestId = getRequestId(headers); + } + + String getRequestId(Metadata headers) { + return GrpcUtil.getRequestId(headers).orElseGet(() -> UUID.randomUUID().toString()); + } + + @Override + public void onMessage(A message) { + doSurroundOn(() -> super.onMessage(message)); + } + + @Override + public void onHalfClose() { + doSurroundOn(super::onHalfClose); + } + + @Override + public void onCancel() { + doSurroundOn(super::onCancel); + } + + @Override + public void onComplete() { + doSurroundOn(super::onComplete); + } + + @Override + public void onReady() { + doSurroundOn(super::onReady); + } + + void doSurroundOn(Runnable runnable) { + try (var ctc = CloseableThreadContext.put(GrpcUtil.KEY_REQUEST_ID, requestId)) { + runnable.run(); + } + } + + } + +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/ExceptionHandler.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ExceptionHandler.java similarity index 98% rename from token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/ExceptionHandler.java rename to token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ExceptionHandler.java index e7f0893ba7ac11c42c3552f2e95fd4804f0f1ce9..118880f673d1b408df85275e398008e7f9a4a1a7 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/ExceptionHandler.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ExceptionHandler.java @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token.errorhandling; +package de.ozgcloud.token.common.errorhandling; import java.util.UUID; diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/FunctionalException.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/FunctionalException.java similarity index 98% rename from token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/FunctionalException.java rename to token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/FunctionalException.java index f7e85d5207d922d307691a9193404ca791dd8aea..467787a14e2abc5d5b43a07ec732103483e8cddf 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/errorhandling/FunctionalException.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/FunctionalException.java @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token.errorhandling; +package de.ozgcloud.token.common.errorhandling; import java.io.Serial; import java.util.Collections; 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 new file mode 100644 index 0000000000000000000000000000000000000000..c1efb9dceb4e20a8b0faec2f7ce71fdc6e308773 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/TokenVerificationException.java @@ -0,0 +1,54 @@ +/* + * 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.common.errorhandling; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.Getter; + +@Getter +public class TokenVerificationException extends TechnicalException { + + private final List<ValidationError> validationErrors; + + public TokenVerificationException(String msg) { + super(msg); + this.validationErrors = Collections.singletonList(ValidationError.builder().message(msg).build()); + } + + public TokenVerificationException(String msg, List<ValidationError> validationErrors) { + super(msg); + this.validationErrors = Objects.isNull(validationErrors) ? Collections.emptyList() : Collections.unmodifiableList(validationErrors); + } + + public TokenVerificationException(String msg, Throwable exception) { + super(msg, exception); + this.validationErrors = Collections.singletonList(ValidationError.builder().message(msg).build()); + } + + @Override + public String getMessage() { + return "[SAML]" + super.getMessage(); + } +} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckResult.java b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ValidationError.java similarity index 85% rename from token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckResult.java rename to token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ValidationError.java index b84173cfdff86dc9e1aace86ffe7388232667ccc..c015d5a49646869ed7f03d2896747618d66183b5 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/TokenCheckResult.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/common/errorhandling/ValidationError.java @@ -21,15 +21,15 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token; - -import java.util.List; +package de.ozgcloud.token.common.errorhandling; import lombok.Builder; +import lombok.Getter; @Builder -public record TokenCheckResult( - String postkorbHandle, - String trustLevel, - List<TokenAttribute> attributes) { +@Getter +public class ValidationError { + + private final String message; + private final Throwable cause; } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/ConfigurationEntity.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/ConfigurationEntity.java index 13797a76b314f72cc2d07bb72eb9fd51c4aa0f00..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/ConfigurationEntity.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/ConfigurationEntity.java @@ -1,77 +0,0 @@ -/* - * 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.Map; - -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; - -import org.springframework.core.io.Resource; -import org.springframework.validation.annotation.Validated; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@ToString -@Validated -public class ConfigurationEntity { - /** - * The id of the Identity Provider, this is also the issuer value. - */ - @NotEmpty - private String idpEntityId; - /** - * The encryption key - */ - @NotEmpty - private Resource key; - /** - * The encryption certificate - */ - @NotEmpty - private Resource certificate; - /** - * The url or the actual SAML Metadata file received from the idp - */ - @NotEmpty - private Resource metadata; - /** - * Use the user id as Postkorbhandle. For Mu - */ - private Boolean useIdAsPostkorbHandle = Boolean.FALSE; - /** - * The mappings the PostfachHandle and the TrustLevel. The value of the mapping the name of the attribute in the SamlResponse that represents - * these values. The default are the mappings used by BayernID. - */ - @NotNull - private Map<String, String> mappings; -} 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 d751a933d7650b2aa358840988481483e9053c4c..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2DecryptionService.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.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.springframework.security.saml2.Saml2Exception; -import org.springframework.stereotype.Service; - -import de.ozgcloud.token.TokenAttribute; -import lombok.NoArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@Service -@NoArgsConstructor -public class Saml2DecryptionService { - - public List<TokenAttribute> decryptAttributes(Response response, SamlConfiguration configuration) { - decryptResponseElements(response, configuration.decrypter()); - - return getAttributes(response); - } - - void decryptResponseElements(Response response, Decrypter decrypter) { - response.getEncryptedAssertions().stream() - .map(encryptedAssertion -> decryptAssertion(encryptedAssertion, decrypter)) - .forEach(assertion -> response.getAssertions().add(assertion)); - } - - private Assertion decryptAssertion(EncryptedAssertion assertion, Decrypter decrypter) { - try { - return decrypter.decrypt(assertion); - } catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - private List<TokenAttribute> 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 TokenAttribute extractNameAndValue(Attribute attribute) { - return TokenAttribute.builder().name(attribute.getName()).value(getAttributeValue(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 0aa430fecaff0a3e1250ef6e2a04d376b5af4681..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2ParseService.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Objects; - -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; -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 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 ResponseUnmarshaller unmarshaller; - - public Response parse(String request) { - return (Response) xmlObject(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); - } - - XMLObject xmlObject(InputStream inputStream) throws Saml2Exception { - try { - var document = parserPool.parse(inputStream); - var element = document.getDocumentElement(); - return getUnmarshaller().unmarshall(element); - } catch (XMLParserException | UnmarshallingException e) { - throw new Saml2Exception("Failed to deserialize LogoutRequest", e); - } - } - - private ResponseUnmarshaller getUnmarshaller() { - if (Objects.nonNull(unmarshaller)) { - return unmarshaller; - } - - unmarshaller = (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() - .getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); - return unmarshaller; - } -} 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 156c27bd8a0132c6163557c43976f15770fee578..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/Saml2VerificationService.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.ArrayList; -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.Signature; -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 INVALID_SIGNATURE_PROFILE = "Invalid signature profile for object"; - public static final String SIGNATURE_MISSING = "Signature missing"; - private static final String FORMAT = " [%s]: "; - - private final Saml2ParseService parser; - private final SamlConfigurationRegistry samlConfigurationRegistry; - - private final SAMLSignatureProfileValidator profileValidator = new SAMLSignatureProfileValidator(); - - public List<Saml2Error> verify(String samlToken) { - var response = parser.parse(samlToken); - var signature = response.getSignature(); - - return getSaml2Errors(signature, response); - } - - private List<Saml2Error> getSaml2Errors(final Signature signature, final Response response) { - List<Saml2Error> errors = new ArrayList<>(); - - if (Objects.nonNull(signature)) { - validateProfile(response, signature, errors); - validateSignature(response, signature, errors); - } else { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, SIGNATURE_MISSING)); - } - return errors; - } - - private void validateProfile(final Response response, final Signature signature, final List<Saml2Error> errors) { - try { - profileValidator.validate(signature); - } catch (SignatureException ex) { - LOG.error("Error validating SAML Token: ", ex); - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE_PROFILE + FORMAT.formatted(response.getID()))); - } - } - - private void validateSignature(final Response response, final Signature signature, final List<Saml2Error> errors) { - try { - var idpEntityId = response.getIssuer().getValue(); - var trustEngine = samlConfigurationRegistry.getConfiguration(idpEntityId).trustEngine(); - if (!trustEngine.validate(signature, samlConfigurationRegistry.getConfiguration(idpEntityId).criteriaSet())) { - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE + FORMAT.formatted(response.getID()))); - } - } catch (SecurityException ex) { - LOG.error("Error validating SAML Token: ", ex); - errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE + FORMAT.formatted(response.getID()))); - } - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlAttributeService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlAttributeService.java new file mode 100644 index 0000000000000000000000000000000000000000..a60402f0cdfa9962226f6bc375a134fd3c3f5b7d --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlAttributeService.java @@ -0,0 +1,187 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +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.TokenAttributes; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import de.ozgcloud.token.common.errorhandling.ValidationError; +import lombok.Builder; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; + +@Builder +public class SamlAttributeService { + + private final SignatureTrustEngine signatureTrustEngine; + private final Decrypter decrypter; + private final SAMLSignatureProfileValidator profileValidator; + private final TokenValidationProperty tokenValidationProperty; + private final CriteriaSet verificationCriteria; + + public TokenAttributes getAttributes(Response token) { + validateToken(token); + return buildTokenAttributes(decryptSamlAttributes(token), token); + } + + void validateToken(Response token) { + if (Objects.isNull(token.getSignature())) { + throw new TokenVerificationException("Token signature is missing"); + } + var validationErrors = new ArrayList<ValidationError>(); + validateSignatureProfile(token.getSignature()).ifPresent(validationErrors::add); + validateSignature(token.getSignature()).ifPresent(validationErrors::add); + if (CollectionUtils.isNotEmpty(validationErrors)) { + throw new TokenVerificationException("Token validation failed", validationErrors); + } + } + + Optional<ValidationError> validateSignatureProfile(Signature signature) { + try { + profileValidator.validate(signature); + } catch (SignatureException e) { + return Optional.of(ValidationError.builder().message("Invalid signature profile: " + e.getMessage()).cause(e).build()); + } + return Optional.empty(); + } + + Optional<ValidationError> validateSignature(Signature signature) { + try { + var isValidSignature = signatureTrustEngine.validate(signature, verificationCriteria); + if (!isValidSignature) { + return Optional.of(ValidationError.builder().message("Invalid token signature").build()); + } + } catch (SecurityException e) { + return Optional.of(ValidationError.builder().message("Error on signature validation: " + e.getMessage()).cause(e).build()); + } + return Optional.empty(); + } + + Map<String, String> decryptSamlAttributes(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(); + }; + } + + TokenAttributes buildTokenAttributes(Map<String, String> tokenAttributes, Response token) { + var tokenAttributesBuilder = TokenAttributes.builder().postfachId(getPostfachId(tokenAttributes, token)) + .trustLevel(getTrustLevel(tokenAttributes)); + tokenAttributes.entrySet().stream().filter(this::isNotNamedAttribute).map(this::buildTokenAttribute) + .forEach(tokenAttributesBuilder::otherAttribute); + return tokenAttributesBuilder.build(); + } + + String getPostfachId(Map<String, String> tokenAttributes, Response token) { + return tokenValidationProperty.isUseIdAsPostfachId() ? token.getID() : tokenAttributes.get(getPostfachIdKey()); + } + + String getTrustLevel(Map<String, String> tokenAttributes) { + return tokenAttributes.get(getTrustLevelKey()); + } + + boolean isNotNamedAttribute(Map.Entry<String, String> attributeEntry) { + return !StringUtils.equalsAny(attributeEntry.getKey(), getPostfachIdKey(), getTrustLevelKey()); + } + + String getPostfachIdKey() { + return tokenValidationProperty.getMappings().getOrDefault(TokenAttributes.POSTFACH_ID_KEY, TokenAttributes.POSTFACH_ID_KEY); + } + + String getTrustLevelKey() { + return tokenValidationProperty.getMappings().getOrDefault(TokenAttributes.TRUST_LEVEL_KEY, TokenAttributes.TRUST_LEVEL_KEY); + } + + TokenAttribute buildTokenAttribute(Map.Entry<String, String> attribute) { + return TokenAttribute.builder().name(getMappedKey(attribute.getKey())).value(attribute.getValue()).build(); + } + + String getMappedKey(String attributeKey) { + return tokenValidationProperty.getMappings().entrySet().stream().filter(entry -> StringUtils.equals(entry.getValue(), attributeKey)) + .map(Map.Entry::getKey).findFirst().orElse(attributeKey); + } +} 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 f11fcd5a49797494e7a003ddd386068051962e81..9cdb801ebaa2e5e3752a5b44fcbd9d9582a97efe 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 @@ -23,27 +23,71 @@ */ package de.ozgcloud.token.saml; -import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.criterion.ProtocolCriterion; +import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion; +import org.opensaml.saml.saml2.core.Response; +import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.criteria.impl.EvaluableEntityIDCredentialCriterion; +import org.opensaml.security.credential.criteria.impl.EvaluableUsageCredentialCriterion; +import org.opensaml.security.criteria.UsageCriterion; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; -import lombok.Builder; +import de.ozgcloud.token.TokenValidationProperties; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import lombok.RequiredArgsConstructor; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -@Builder -public record SamlConfiguration( - SignatureTrustEngine trustEngine, - CriteriaSet criteriaSet, - Decrypter decrypter, - Map<String, String> mappings, - boolean idIsPostKorbMapping) { - - @Override - public String toString() { - return "SamlConfiguration{" + - "mappings=" + mappings + - ", idIsPostKorbMapping=" + idIsPostKorbMapping + - '}'; +@Configuration +@RequiredArgsConstructor +public class SamlConfiguration { + + private final SamlTrustEngineFactory samlTrustEngineFactory; + private final SamlDecrypterFactory samlDecrypterFactory; + + @Bean + ResponseUnmarshaller responseUnmarshaller() { + return (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); + } + + @Bean + @DependsOn("xmlObjectProviderRegistry") + SamlServiceRegistry samlServiceRegistry(TokenValidationProperties tokenValidationProperties) { + var registryBuilder = SamlServiceRegistry.builder(); + for (var tokenEntity : tokenValidationProperties.getEntities()) { + registryBuilder.samlService(tokenEntity.getIdpEntityId(), samlAttributeService(tokenEntity)); + } + return registryBuilder.build(); + } + + SamlAttributeService samlAttributeService(TokenValidationProperty tokenValidationProperty) { + return SamlAttributeService.builder() + .signatureTrustEngine(samlTrustEngineFactory.buildSamlTrustEngine(tokenValidationProperty)) + .decrypter(samlDecrypterFactory.buildDecrypter(tokenValidationProperty)) + .profileValidator(samlSignatureProfileValidator()) + .tokenValidationProperty(tokenValidationProperty) + .verificationCriteria(buildVerificationCriteria(tokenValidationProperty.getIdpEntityId())) + .build(); } + + SAMLSignatureProfileValidator samlSignatureProfileValidator() { + return new SAMLSignatureProfileValidator(); + } + + CriteriaSet buildVerificationCriteria(String idpEntityId) { + return Stream.of( + new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(idpEntityId)), + new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion("urn:oasis:names:tc:SAML:2.0:protocol")), + new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)) + ).collect(Collectors.toCollection(CriteriaSet::new)); + } + } diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfigurationRegistry.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfigurationRegistry.java index 220de72a5755fa4329320eebac45e698ffcfe8f3..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfigurationRegistry.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlConfigurationRegistry.java @@ -1,49 +0,0 @@ -/* - * 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.HashMap; -import java.util.Map; - -import org.springframework.stereotype.Service; -import org.springframework.util.Assert; - -@Service -public class SamlConfigurationRegistry { - private final Map<String, SamlConfiguration> samlConfigurations = new HashMap<>(); - - public void addConfiguration(String idpEntityId, SamlConfiguration configuration) { - samlConfigurations.put(idpEntityId, configuration); - } - - public SamlConfiguration getConfiguration(String idpEntityId) { - var samlConfiguration = samlConfigurations.get(idpEntityId); - - Assert.state(samlConfiguration != null, - "Saml2 Configuration for " + idpEntityId - + " is empty. SamlConfigurationRegistry not proper initialized or no SAML2 Identity Provider configured"); - - return samlConfiguration; - } -} diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlDecrypterFactory.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlDecrypterFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9159a5ff12f53c6745e4c9bad9dc32354a266a72 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlDecrypterFactory.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.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.List; + +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.saml.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.EncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.InlineEncryptedKeyResolver; +import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; +import org.springframework.security.converter.RsaKeyConverters; +import org.springframework.stereotype.Component; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; + +@Component +class SamlDecrypterFactory { + + private static final String X509_CERTIFICATE_TYPE = "X.509"; + + public Decrypter buildDecrypter(TokenValidationProperty tokenValidationProperty) { + return DecrypterBuilder.builder() + .keyEncryptionKeyResolver(buildKeyInfoCredentialResolver(tokenValidationProperty)) + .encryptedKeyElementsResolver(buildEncryptedKeyResolver()) + .build(); + } + + CollectionKeyInfoCredentialResolver buildKeyInfoCredentialResolver(TokenValidationProperty tokenValidationProperty) { + return new CollectionKeyInfoCredentialResolver( + List.of(CredentialSupport.getSimpleCredential(getCertificate(tokenValidationProperty), getPrivateKey(tokenValidationProperty)))); + } + + X509Certificate getCertificate(TokenValidationProperty tokenValidationProperty) { + try (var inputStream = tokenValidationProperty.getCertificate().getInputStream()) { + return (X509Certificate) CertificateFactory.getInstance(X509_CERTIFICATE_TYPE).generateCertificate(inputStream); + } catch (IOException | CertificateException e) { + throw new TechnicalException("Cannot read certificate", e); + } + } + + RSAPrivateKey getPrivateKey(TokenValidationProperty tokenValidationProperty) { + try (var inputStream = tokenValidationProperty.getKey().getInputStream()) { + return RsaKeyConverters.pkcs8().convert(inputStream); + } catch (IOException e) { + throw new TechnicalException("Cannot read encryption key", e); + } + } + + ChainingEncryptedKeyResolver buildEncryptedKeyResolver() { + return new ChainingEncryptedKeyResolver(List.of(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), + new SimpleRetrievalMethodEncryptedKeyResolver())); + } + + static class DecrypterBuilder { + + static DecrypterBuilder builder() { + return new DecrypterBuilder(); + } + + private KeyInfoCredentialResolver keyEncryptionKeyResolver; + private EncryptedKeyResolver encryptedKeyElementsResolver; + + DecrypterBuilder keyEncryptionKeyResolver(KeyInfoCredentialResolver keyEncryptionKeyResolver) { + this.keyEncryptionKeyResolver = keyEncryptionKeyResolver; + return this; + } + + DecrypterBuilder encryptedKeyElementsResolver(EncryptedKeyResolver encryptedKeyElementsResolver) { + this.encryptedKeyElementsResolver = encryptedKeyElementsResolver; + return this; + } + + Decrypter build() { + var decrypter = new Decrypter(null, keyEncryptionKeyResolver, encryptedKeyElementsResolver); + decrypter.setRootInNewDocument(true); + return decrypter; + } + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckApplicationTest.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlServiceRegistry.java similarity index 71% rename from token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckApplicationTest.java rename to token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlServiceRegistry.java index 5158321a1ea6514c40fb5e645d6b376b7c0c0a36..c64070614af39e7c9d7347fee0bf2e6c73c18038 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckApplicationTest.java +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlServiceRegistry.java @@ -21,22 +21,23 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token; +package de.ozgcloud.token.saml; import static org.assertj.core.api.Assertions.*; -import org.junit.jupiter.api.Test; +import java.util.Map; +import java.util.Optional; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import lombok.Builder; +import lombok.Singular; -class TokenCheckApplicationTest { +@Builder +public class SamlServiceRegistry { - @Test - void shouldCreateParserPool() throws ComponentInitializationException { - TokenCheckApplication application = new TokenCheckApplication(); + @Singular + private final Map<String, SamlAttributeService> samlServices; - var parserPool = application.parserPool(); - - assertThat(parserPool).isNotNull(); + public Optional<SamlAttributeService> getService(String idpEntityId) { + return Optional.ofNullable(samlServices.get(idpEntityId)); } -} \ No newline at end of file +} 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..880ac20978843b46e132d0f8cae62f70894563b0 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenService.java @@ -0,0 +1,105 @@ +/* + * 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.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +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.TokenAttributes; +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(getAttributes(parseToken(token))); + } catch (TokenVerificationException e) { + LOG.debug("Token validation failed", e); + e.getValidationErrors().forEach(validationError -> LOG.error(validationError.getMessage(), validationError.getCause())); + return buildInvalidTokenResult(e); + } + } + + Response parseToken(String token) { + try (var inputStream = buildInputStream(token)) { + 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); + } + } + + InputStream buildInputStream(String token) { + return new ByteArrayInputStream(token.getBytes(StandardCharsets.UTF_8)); + } + + TokenAttributes getAttributes(Response token) { + return getSamlAttributeService(getTokenIssuer(token)).getAttributes(token); + } + + String getTokenIssuer(Response token) { + return Optional.ofNullable(token.getIssuer()).map(Issuer::getValue) + .orElseThrow(() -> new TokenVerificationException("No token issuer found")); + } + + SamlAttributeService getSamlAttributeService(String tokenIssuer) { + return samlServiceRegistry.getService(tokenIssuer) + .orElseThrow(() -> new TechnicalException("Can't validate token with issuer %s".formatted(tokenIssuer))); + } + + TokenValidationResult buildValidTokenResult(TokenAttributes tokenAttributes) { + return TokenValidationResult.builder().valid(true).attributes(tokenAttributes).build(); + } + + TokenValidationResult buildInvalidTokenResult(TokenVerificationException exception) { + return TokenValidationResult.builder() + .valid(false) + .validationErrors(exception.getValidationErrors()) + .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 a69fd70faca2851537c3807dba679b3e10960149..0000000000000000000000000000000000000000 --- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenUtils.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.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 FEATURES_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities"; - public static final String FEATURES_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities"; - public static final String FEATURES_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl"; - public static final String VALIDATION_SCHEMA_NORMALIZED_VALUE = "http://apache.org/xml/features/validation/schema/normalized-value"; - public static final String FEATURE_SECURE_PROCESSING = "http://javax.xml.XMLConstants/feature/secure-processing"; - - 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/SamlTrustEngineFactory.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..fd9567a9b7ecd15cd714468c7c175416fcee73b1 --- /dev/null +++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java @@ -0,0 +1,153 @@ +/* + * 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.IOException; +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallingException; +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.Credential; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.springframework.core.io.Resource; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.stereotype.Component; +import org.w3c.dom.Element; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import lombok.RequiredArgsConstructor; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; + +@Component +@RequiredArgsConstructor +class SamlTrustEngineFactory { + + private final XMLObjectProviderRegistry registry; + private final ParserPool parserPool; + + public SignatureTrustEngine buildSamlTrustEngine(TokenValidationProperty tokenValidationProperty) { + return new ExplicitKeySignatureTrustEngine(buildCredentialResolver(tokenValidationProperty), + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + } + + CollectionCredentialResolver buildCredentialResolver(TokenValidationProperty entity) { + 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"); + } + return new CollectionCredentialResolver(credentials); + } + + Stream<Saml2X509Credential> getCertificatesFromMetadata(Resource metadata) { + try (var metadataInputStream = metadata.getInputStream()) { + return findEntityDescriptor(getXmlObject(metadataInputStream)) + .map(this::getVerificationCertificates) + .orElseThrow(() -> new TechnicalException("No IDPSSO Descriptors found")); + } catch (IOException e) { + throw new TechnicalException("Error reading idp metadata.", e); + } + } + + XMLObject getXmlObject(InputStream inputStream) { + try { + var element = parserPool.parse(inputStream).getDocumentElement(); + return getUnmarshaller(element).unmarshall(element); + } catch (XMLParserException | UnmarshallingException e) { + throw new TechnicalException("Cannot parse metadata", e); + } + } + + Unmarshaller getUnmarshaller(Element element) { + var unmarshallerFactory = registry.getUnmarshallerFactory(); + return Optional.ofNullable(unmarshallerFactory.getUnmarshaller(element)) + .orElseThrow(() -> new TechnicalException("Unsupported element of type " + element.getTagName())); + } + + Optional<EntityDescriptor> findEntityDescriptor(XMLObject metadata) { + return extractEntityDescriptor(metadata).filter( + entityDescriptor -> Objects.nonNull(entityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS))); + } + + Optional<EntityDescriptor> extractEntityDescriptor(XMLObject metadata) { + if (metadata instanceof EntityDescriptor entityDescriptor) { + return Optional.of(entityDescriptor); + } else if (metadata instanceof EntitiesDescriptor entitiesDescriptor) { + return entitiesDescriptor.getEntityDescriptors().stream().findFirst(); + } + return Optional.empty(); + } + + Stream<Saml2X509Credential> getVerificationCertificates(EntityDescriptor descriptor) { + var idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + if (Objects.isNull(idpssoDescriptor)) { + throw new TechnicalException("Metadata response is missing the necessary IDPSSODescriptor element"); + } + return idpssoDescriptor.getKeyDescriptors().stream() + .filter(this::isSignatureKey) + .flatMap(this::getCertificates) + .map(Saml2X509Credential::verification); + } + + boolean isSignatureKey(KeyDescriptor keyDescriptor) { + return keyDescriptor.getUse() == UsageType.SIGNING; + } + + Stream<X509Certificate> getCertificates(KeyDescriptor keyDescriptor) { + try { + return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo()).stream(); + } catch (CertificateException e) { + throw new TechnicalException("Error reading certificates from key descriptor", e); + } + } + + Credential buildBasicX509Credential(Saml2X509Credential key, String idpEntityId) { + var cred = new BasicX509Credential(key.getCertificate()); + cred.setUsageType(UsageType.SIGNING); + cred.setEntityId(idpEntityId); + return cred; + } + +} diff --git a/token-checker-server/src/main/resources/application-local.yml b/token-checker-server/src/main/resources/application-local.yml index dbd7b1cf4be3b8bec590cc43b9d4615e2510d817..d601973b2998caf553a9041db64203c0ce880256 100644 --- a/token-checker-server/src/main/resources/application-local.yml +++ b/token-checker-server/src/main/resources/application-local.yml @@ -1,8 +1,10 @@ logging: level: - "net.devh.boot.grpc": INFO + ROOT: ERROR, + "org.springframework": ERROR "org.opensaml.xmlsec.encryption.support": DEBUG - config: "token-checker-server/src/main/resources/log4j2-local.xml" + "de.ozgcloud": INFO + config: classpath:log4j2-local.xml ozgcloud: token: check: @@ -12,13 +14,13 @@ ozgcloud: certificate: "classpath:test1-enc.crt" metadata: "classpath:metadata/bayernid-idp-infra.xml" mappings: - postkorbHandle: "urn:oid:2.5.4.18" + postfachId: "urn:oid:2.5.4.18" trustLevel: "urn:oid:1.2.40.0.10.2.1.1.261.94" - idpEntityId: "https://e4k-portal.een.elster.de" key: "classpath:test2-enc.key" certificate: "classpath:test2-enc.crt" metadata: "classpath:metadata/muk-idp-e4k.xml" - useIdAsPostkorbHandle: true + useIdAsPostfachId: true mappings: trustLevel: "ElsterVertrauensniveauAuthentifizierung" server: diff --git a/token-checker-server/src/test/helm/configmap_bindings_type_test.yaml b/token-checker-server/src/test/helm/configmap_bindings_type_test.yaml index bf7fcbb706aaaca61a59f18f56a67631080134c4..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/test/helm/configmap_bindings_type_test.yaml +++ b/token-checker-server/src/test/helm/configmap_bindings_type_test.yaml @@ -1,47 +0,0 @@ -# -# 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. -# - -suite: configmap_bindings_type -release: - name: token-checker-server - namespace: sh-helm-test -templates: - - templates/configmap_bindings_type.yaml - -tests: - - it: validate configmap values - asserts: - - isKind: - of: ConfigMap - - isAPIVersion: - of: v1 - - equal: - path: metadata.name - value: token-checker-server-bindings-type - - equal: - path: metadata.namespace - value: sh-helm-test - - equal: - path: data.type - value: "ca-certificates" \ No newline at end of file diff --git a/token-checker-server/src/test/helm/deployment_bindings_test.yaml b/token-checker-server/src/test/helm/deployment_bindings_test.yaml index 08ec93603e5de5a4df80ca56d90ed1ce8a7c2934..6a348eec73f55bed0b25421e2d11a2f3debf28ad 100644 --- a/token-checker-server/src/test/helm/deployment_bindings_test.yaml +++ b/token-checker-server/src/test/helm/deployment_bindings_test.yaml @@ -41,24 +41,6 @@ set: trustLevel: TrustLevelNameUsedByIdp tests: - - it: should have bindings volumeMounts on it's container - asserts: - - contains: - path: spec.template.spec.containers[0].volumeMounts - content: - name: bindings - mountPath: "/bindings/ca-certificates/type" - subPath: type - readOnly: true - - it: should have bindings volumes - asserts: - - contains: - path: spec.template.spec.volumes - content: - name: "bindings" - configMap: - name: token-checker-server-bindings-type - - it: should have volume for saml secrets asserts: - contains: @@ -99,4 +81,4 @@ tests: samlRegistrationSecretName: asserts: - failedTemplate: - errormessage: .Values.samlRegistrationSecretName must be set \ No newline at end of file + errormessage: .Values.samlRegistrationSecretName must be set diff --git a/token-checker-server/src/test/helm/deployment_env_test.yaml b/token-checker-server/src/test/helm/deployment_env_test.yaml index 2872d2065685600785805ce9fcc6763899de5a72..1711e8e9d1eb4341ba288587f597fb6a757193ab 100644 --- a/token-checker-server/src/test/helm/deployment_env_test.yaml +++ b/token-checker-server/src/test/helm/deployment_env_test.yaml @@ -98,24 +98,6 @@ tests: name: my_test_environment_name value: "A test value" - - it: should have SERVICE_BINDING_ROOT - set: - imagePullSecret: test-image-secret - samlRegistrationSecretName: muk-saml-registration-secret - ozgcloud: - environment: dev - tokenChecker: - entities: - - idpEntityId: https://idp-id - mappings: - trustLevel: TrustLevelNameUsedByIdp - asserts: - - contains: - path: spec.template.spec.containers[0].env - content: - name: SERVICE_BINDING_ROOT - value: "/bindings" - - it: should contain saml envs set: imagePullSecret: test-image-secret @@ -148,11 +130,6 @@ tests: content: name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_METADATA value: file:///metadata/muk-idp-infra.xml - - contains: - path: spec.template.spec.containers[0].env - content: - name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_USE-ID-AS-POSTKORB-HANDLE - value: "true" - contains: path: spec.template.spec.containers[0].env content: @@ -168,7 +145,7 @@ tests: tokenChecker: entities: - mappings: - trustLevel: TrustLevelNameUsedByIdp + trustLevel: TrustLevelNameUsedByIdp asserts: - failedTemplate: errormessage: ozgcloud.tokenChecker.entities idp entity id must be set @@ -181,11 +158,83 @@ tests: environment: dev tokenChecker: entities: - - idpEntityId: https://idp-id + - idpEntityId: https://idp-id mappings: trustLevel: asserts: - failedTemplate: errormessage: "at least one ozgcloud.token.check.entities.mappings trustlevel must be set" - \ No newline at end of file + - it: should set default for useIdAsPostfachId + set: + env.customList: + - name: my_test_environment_name + value: "A test value" + - name: test_environment + value: "B test value" + imagePullSecret: test-image-secret + samlRegistrationSecretName: muk-saml-registration-secret + ozgcloud: + environment: dev + tokenChecker: + entities: + - idpEntityId: https://idp-id + mappings: + trustLevel: TrustLevelNameUsedByIdp + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_USE-ID-AS-POSTFACH-ID + value: "true" + + - it: should set custom mapping for PostfachId + set: + env.customList: + - name: my_test_environment_name + value: "A test value" + - name: test_environment + value: "B test value" + imagePullSecret: test-image-secret + samlRegistrationSecretName: muk-saml-registration-secret + ozgcloud: + environment: dev + tokenChecker: + entities: + - idpEntityId: https://idp-id + useIdAsPostfachId: false + mappings: + trustLevel: TrustLevelNameUsedByIdp + postfachId: PostfachIdNameUsedByIdp + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_USE-ID-AS-POSTFACH-ID + value: "false" + - contains: + path: spec.template.spec.containers[0].env + content: + name: OZGCLOUD_TOKEN_CHECK_ENTITIES_0_MAPPINGS_POSTFACH-ID + value: PostfachIdNameUsedByIdp + + - it: should set fail due to missing mapping for PostfachId + set: + env.customList: + - name: my_test_environment_name + value: "A test value" + - name: test_environment + value: "B test value" + imagePullSecret: test-image-secret + samlRegistrationSecretName: muk-saml-registration-secret + ozgcloud: + environment: dev + tokenChecker: + entities: + - idpEntityId: https://idp-id + useIdAsPostfachId: false + mappings: + trustLevel: TrustLevelNameUsedByIdp + asserts: + - failedTemplate: + errormessage: "at least one ozgcloud.token.check.entities.mappings postfachId must be set" diff --git a/token-checker-server/src/test/helm/service_account_test.yaml b/token-checker-server/src/test/helm/service_account_test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bbbe2e07f534e21bb8c76da3b66a1dc017a65f78 --- /dev/null +++ b/token-checker-server/src/test/helm/service_account_test.yaml @@ -0,0 +1,40 @@ +suite: test service account +release: + name: token-checker + namespace: sh-helm-test +templates: + - templates/service_account.yaml +tests: + - it: should create service account with default name + set: + serviceAccount: + create: true + asserts: + - isKind: + of: ServiceAccount + - isAPIVersion: + of: v1 + - equal: + path: metadata.name + value: token-checker-service-account + - equal: + path: metadata.namespace + value: sh-helm-test + - it: should create service account with name + set: + serviceAccount: + create: true + name: helm-service-account + asserts: + - isKind: + of: ServiceAccount + - equal: + path: metadata.name + value: helm-service-account + - equal: + path: metadata.namespace + value: sh-helm-test + - it: should not create service account + asserts: + - hasDocuments: + count: 0 diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..58e1a2a69a08fe050948e8e32d6f4ebdc1552a13 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.token; + +import de.ozgcloud.token.GrpcCheckError.Builder; +import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory; + +public class GrpcCheckErrorTestFactory { + + public static final String ERROR_MESSAGE = ValidationErrorTestFactory.ERROR_MESSAGE; + + public static GrpcCheckError create() { + return createBuilder().build(); + } + + public static Builder createBuilder() { + return GrpcCheckError.newBuilder().setMessage(ERROR_MESSAGE); + } + +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorsTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorsTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..b04386bc696719c0669b73f526c61b4f10bec675 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckErrorsTestFactory.java @@ -0,0 +1,17 @@ +package de.ozgcloud.token; + +import de.ozgcloud.token.GrpcCheckErrors.Builder; + +public class GrpcCheckErrorsTestFactory { + + public static final GrpcCheckError CHECK_ERROR = GrpcCheckErrorTestFactory.create(); + + public static GrpcCheckErrors create() { + return createBuilder().build(); + } + + public static Builder createBuilder() { + return GrpcCheckErrors.newBuilder().addCheckError(CHECK_ERROR); + } + +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckRequestTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenRequestTestFactory.java similarity index 78% rename from token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckRequestTestFactory.java rename to token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenRequestTestFactory.java index 145e869b8bc256eceee7b4fd70ac67d7b6b0c90a..19da316fcd2445db7ba6d13793544ff604a44153 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckRequestTestFactory.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenRequestTestFactory.java @@ -24,18 +24,16 @@ package de.ozgcloud.token; import de.ozgcloud.common.test.TestUtils; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -class GrpcTokenCheckRequestTestFactory { +public class GrpcCheckTokenRequestTestFactory { + static final String TOKEN = TestUtils.loadTextFile("SamlResponseMuk.xml"); - static GrpcTokenCheckRequest create() { + static GrpcCheckTokenRequest create() { return createBuilder().build(); } - static GrpcTokenCheckRequest.Builder createBuilder() { - return GrpcTokenCheckRequest.newBuilder().setToken(TOKEN); + static GrpcCheckTokenRequest.Builder createBuilder() { + return GrpcCheckTokenRequest.newBuilder().setToken(TOKEN); } } diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenResponseTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenResponseTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c358b23705a819ab6f89495f8f3a43c570732a50 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcCheckTokenResponseTestFactory.java @@ -0,0 +1,47 @@ +/* + * 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; + +class GrpcCheckTokenResponseTestFactory { + + public static final GrpcTokenAttributes GRPC_TOKEN_ATTRIBUTE = GrpcTokenAttributesTestFactory.create(); + public static final GrpcCheckErrors GRPC_CHECK_ERRORS = GrpcCheckErrorsTestFactory.create(); + + public static GrpcCheckTokenResponse createValid() { + return createValidBuilder().build(); + } + + public static GrpcCheckTokenResponse.Builder createValidBuilder() { + return GrpcCheckTokenResponse.newBuilder() + .setTokenValid(true) + .setTokenAttributes(GRPC_TOKEN_ATTRIBUTE); + } + + public static GrpcCheckTokenResponse createInvalid() { + return createInvalidBuilder().build(); + } + + public static GrpcCheckTokenResponse.Builder createInvalidBuilder() { + return GrpcCheckTokenResponse.newBuilder() + .setTokenValid(false) + .setCheckErrors(GRPC_CHECK_ERRORS); + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcOtherFieldTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcOtherFieldTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..6ddcb93f30f64d0c6a0ec883c9beb3a7f2aa06ee --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcOtherFieldTestFactory.java @@ -0,0 +1,18 @@ +package de.ozgcloud.token; + +public class GrpcOtherFieldTestFactory { + + public static final String VALUE = TokenAttributeTestFactory.VALUE; + public static final String NAME = TokenAttributeTestFactory.NAME; + + public static GrpcOtherField create() { + return createBuilder().build(); + } + + private static GrpcOtherField.Builder createBuilder() { + return GrpcOtherField.newBuilder() + .setName(NAME) + .setValue(VALUE); + } + +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenAttributesTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenAttributesTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..853f730163095b9abb6707afc3bcf8b614636c04 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenAttributesTestFactory.java @@ -0,0 +1,42 @@ +/* + * 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; + +class GrpcTokenAttributesTestFactory { + + public static final String POSTFACH_ID = TokenAttributesTestFactory.POSTFACH_ID; + public static final String TRUST_LEVEL = TokenAttributesTestFactory.TRUST_LEVEL; + public static final GrpcOtherField TOKEN_ATTRIBUTE = GrpcOtherFieldTestFactory.create(); + + public static GrpcTokenAttributes create() { + return createBuilder().build(); + } + + public static GrpcTokenAttributes.Builder createBuilder() { + return GrpcTokenAttributes.newBuilder() + .setPostfachId(POSTFACH_ID) + .setTrustLevel(TRUST_LEVEL) + .addOtherFields(TOKEN_ATTRIBUTE); + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckServiceTest.java index 34e8739355ef2a00f600514bea62b0cc6e7be23c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckServiceTest.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckServiceTest.java @@ -1,89 +0,0 @@ -/* - * 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 static org.mockito.Mockito.*; - -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; - -import io.grpc.stub.StreamObserver; - -class GrpcTokenCheckServiceTest { - @Spy - @InjectMocks - private GrpcTokenCheckService grpcTokenCheckerService; - - @Mock - private TokenCheckService tokenCheckerService; - - @Mock - private TokenCheckMapper tokenCheckMapper; - - @Mock - private StreamObserver<GrpcTokenCheckResponse> tokenStreamObserver; - - @Test - void shouldCallService() { - grpcTokenCheckerService.checkToken(GrpcTokenCheckRequestTestFactory.create(), tokenStreamObserver); - - verify(tokenCheckerService).checkToken(anyString()); - } - - @Test - void shouldCallMapper() { - when(tokenCheckerService.checkToken(anyString())).thenReturn(TokenCheckResultTestFactory.create()); - - grpcTokenCheckerService.checkToken(GrpcTokenCheckRequestTestFactory.create(), tokenStreamObserver); - - verify(tokenCheckMapper).toGrpcTokenResponse(any(TokenCheckResult.class)); - } - - @Test - void shouldCallOnNext() { - when(tokenCheckMapper.toGrpcTokenResponse(any())).thenReturn(GrpcTokenCheckResponseTestFactory.create()); - - grpcTokenCheckerService.checkToken(GrpcTokenCheckRequestTestFactory.create(), tokenStreamObserver); - - verify(tokenStreamObserver).onNext(any(GrpcTokenCheckResponse.class)); - } - - @Test - void shouldCallOnCompleted() { - grpcTokenCheckerService.checkToken(GrpcTokenCheckRequestTestFactory.create(), tokenStreamObserver); - - verify(tokenStreamObserver).onCompleted(); - } - - @Test - void shouldCallOnError() { - doThrow(RuntimeException.class).when(tokenCheckMapper).toGrpcTokenResponse(any()); - - grpcTokenCheckerService.checkToken(GrpcTokenCheckRequestTestFactory.create(), tokenStreamObserver); - - verify(tokenStreamObserver).onError(any()); - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..a18272b442c68fff58524314f20fbf061cbe984b --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java @@ -0,0 +1,49 @@ +/* + * 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.HashMap; +import java.util.Map; + +import com.thedeanda.lorem.LoremIpsum; + +public class TokenAttributeTestFactory { + + public static final String NAME = LoremIpsum.getInstance().getWords(1); + public static final String VALUE = LoremIpsum.getInstance().getWords(1); + + public static TokenAttribute create() { + return createBuilder().build(); + } + + public static TokenAttribute.TokenAttributeBuilder createBuilder() { + return TokenAttribute.builder() + .name(NAME) + .value(VALUE); + } + + public static Map<String, String> asMap() { + return new HashMap<>(Map.of(NAME, VALUE)); + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckResultTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributesTestFactory.java similarity index 54% rename from token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckResultTestFactory.java rename to token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributesTestFactory.java index 11cff2ae8cb40097228187088c5f8f99227edcc8..7e4e4d2fc7c5b02f9b8d77f754fd1875c8e94277 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckResultTestFactory.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributesTestFactory.java @@ -21,29 +21,36 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ + package de.ozgcloud.token; -import java.util.List; +import java.util.Map; +import java.util.UUID; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import com.thedeanda.lorem.LoremIpsum; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class TokenCheckResultTestFactory { +public class TokenAttributesTestFactory { - public static final String POSTKORB_HANDLE = "123-456-789"; - public static final String TRUST_LEVEL = "LOW"; - public static final List<TokenAttribute> OTHER_FIELDS = List.of( - new TokenAttribute("otherField1", "otherValue1"), new TokenAttribute("otherField2", "otherValue2")); + public static final String POSTFACH_ID = UUID.randomUUID().toString(); + public static final String TRUST_LEVEL = LoremIpsum.getInstance().getWords(1); + public static final TokenAttribute OTHER_ATTRIBUTE = TokenAttributeTestFactory.create(); - static TokenCheckResult create() { + public static TokenAttributes create() { return createBuilder().build(); } - static TokenCheckResult.TokenCheckResultBuilder createBuilder() { - return new TokenCheckResult.TokenCheckResultBuilder() - .postkorbHandle(POSTKORB_HANDLE) - .trustLevel(TRUST_LEVEL) - .attributes(OTHER_FIELDS); + public static TokenAttributes.TokenAttributesBuilder createBuilder() { + return TokenAttributes.builder() + .postfachId(POSTFACH_ID) + .trustLevel(TRUST_LEVEL) + .otherAttribute(OTHER_ATTRIBUTE); + } + + public static Map<String, String> asMap() { + return Map.of( + TokenAttributes.POSTFACH_ID_KEY, POSTFACH_ID, + TokenAttributes.TRUST_LEVEL_KEY, TRUST_LEVEL, + TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE + ); } } diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceITCase.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..1e2e4fbb5a6073b4866e80c09a64379dfcaa1621 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceITCase.java @@ -0,0 +1,115 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; + +import de.ozgcloud.common.test.ITCase; +import de.ozgcloud.common.test.TestUtils; +import de.ozgcloud.token.saml.SamlTestConfiguration; +import lombok.SneakyThrows; +import net.devh.boot.grpc.client.inject.GrpcClient; + +@ITCase +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", + "grpc.client.token-checker.address=in-process:test", +}) +@DirtiesContext +public class TokenCheckGrpcServiceITCase { + + @GrpcClient("token-checker") + private TokenCheckServiceGrpc.TokenCheckServiceBlockingStub tokenCheckerStub; + + @MockBean + private SignatureTrustEngine signatureTrustEngine; + + @Nested + class TestCheckTokenSuccessfully { + + @SneakyThrows + @BeforeEach + void init() { + when(signatureTrustEngine.validate(any(), any())).thenReturn(true); + } + + @Test + void shouldAcceptToken() { + var token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); + + var result = tokenCheckerStub.checkToken(buildCheckTokenRequest(token)); + + assertThat(result.getTokenValid()).isTrue(); + } + + @Test + void shouldSetClientAttribute() { + var token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); + + var result = tokenCheckerStub.checkToken(buildCheckTokenRequest(token)); + + assertThat(result.getTokenAttributes().getPostfachId()).isEqualTo(SamlTestConfiguration.POSTFACH_ID); + assertThat(result.getTokenAttributes().getTrustLevel()).isEqualTo(SamlTestConfiguration.TRUST_LEVEL); + assertThat(getOtherFields(result)).hasSize(4).doesNotContainKeys(TokenAttributes.POSTFACH_ID_KEY, TokenAttributes.TRUST_LEVEL_KEY); + } + + private Map<String, String> getOtherFields(GrpcCheckTokenResponse result) { + return result.getTokenAttributes().getOtherFieldsList().stream() + .collect(Collectors.toMap(GrpcOtherField::getName, GrpcOtherField::getValue)); + } + } + + @Nested + class TestCheckTokenFailure { + + @Test + void shouldDeclineToken() { + var token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); + + var result = tokenCheckerStub.checkToken(buildCheckTokenRequest(token)); + + assertThat(result.getTokenValid()).isFalse(); + assertThat(result.getCheckErrors().getCheckErrorList()).hasSize(1).first().extracting(GrpcCheckError::getMessage).asString() + .contains("Invalid token signature"); + } + + } + + private GrpcCheckTokenRequest buildCheckTokenRequest(String token) { + return GrpcCheckTokenRequest.newBuilder().setToken(token).build(); + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1783dd2bd1945fd46d0f32de0220e6a590c764f8 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckGrpcServiceTest.java @@ -0,0 +1,212 @@ +/* + * 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 org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +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 de.ozgcloud.token.saml.SamlTokenService; +import io.grpc.stub.StreamObserver; + +class TokenCheckGrpcServiceTest { + + @Spy + @InjectMocks + private TokenCheckGrpcService service; + + @Mock + private SamlTokenService samlTokenService; + + @Mock + private TokenValidationResultMapper tokenCheckMapper; + + @Mock + private StreamObserver<GrpcCheckTokenResponse> tokenStreamObserver; + @Mock + private GrpcCheckTokenResponse grpcCheckTokenResponse; + + @Nested + class TestCheckToken { + + @Mock + private TokenValidationResult tokenCheckResult; + + @BeforeEach + void init() { + when(samlTokenService.validate(any())).thenReturn(tokenCheckResult); + doReturn(grpcCheckTokenResponse).when(service).buildCheckResponse(any()); + } + + @Test + void shouldCallSamlTokenService() { + checkToken(); + + verify(samlTokenService).validate(GrpcCheckTokenRequestTestFactory.TOKEN); + } + + @Test + void shouldCallBuildCheckResponse() { + checkToken(); + + verify(service).buildCheckResponse(tokenCheckResult); + } + + @Test + void shouldCallOnNext() { + checkToken(); + + verify(tokenStreamObserver).onNext(grpcCheckTokenResponse); + } + + @Test + void shouldCallOnCompleted() { + checkToken(); + + verify(tokenStreamObserver).onCompleted(); + } + + private void checkToken() { + service.checkToken(GrpcCheckTokenRequestTestFactory.create(), tokenStreamObserver); + } + } + + @Nested + class TestBuildCheckResponse { + + @Nested + class TestValidToken { + + private final TokenValidationResult tokenCheckResult = TokenValidationResultTestFactory.createValid(); + + @BeforeEach + void mock() { + doReturn(grpcCheckTokenResponse).when(service).buildValidCheckTokenResponse(any()); + } + + @Test + void shouldCallBuildValidCheckTokenResponse() { + service.buildCheckResponse(tokenCheckResult); + + verify(service).buildValidCheckTokenResponse(tokenCheckResult); + verify(service, never()).buildInvalidCheckTokenResponse(any()); + } + + @Test + void shouldReturnResponse() { + var result = service.buildCheckResponse(tokenCheckResult); + + assertThat(result).isEqualTo(grpcCheckTokenResponse); + } + } + + @Nested + class TestInvalidToken { + + private final TokenValidationResult tokenCheckResult = TokenValidationResultTestFactory.createInvalid(); + + @BeforeEach + void mock() { + doReturn(grpcCheckTokenResponse).when(service).buildInvalidCheckTokenResponse(any()); + } + + @Test + void shouldCallBuildInvalidCheckTokenResponse() { + service.buildCheckResponse(tokenCheckResult); + + verify(service).buildInvalidCheckTokenResponse(tokenCheckResult); + verify(service, never()).buildValidCheckTokenResponse(any()); + } + + @Test + void shouldReturnResponse() { + var result = service.buildCheckResponse(tokenCheckResult); + + assertThat(result).isEqualTo(grpcCheckTokenResponse); + } + } + } + + @Nested + class TestBuildValidCheckTokenResponse { + + private final TokenValidationResult tokenCheckResult = TokenValidationResultTestFactory.createValid(); + + @BeforeEach + void mock() { + when(tokenCheckMapper.toTokenAttributes(any())).thenReturn(GrpcCheckTokenResponseTestFactory.GRPC_TOKEN_ATTRIBUTE); + } + + @Test + void shouldCallMapper() { + buildCheckTokenResponse(); + + verify(tokenCheckMapper).toTokenAttributes(tokenCheckResult); + } + + @Test + void shouldReturnResponse() { + var response = buildCheckTokenResponse(); + + assertThat(response).usingRecursiveComparison().isEqualTo(GrpcCheckTokenResponseTestFactory.createValid()); + } + + private GrpcCheckTokenResponse buildCheckTokenResponse() { + return service.buildValidCheckTokenResponse(tokenCheckResult); + } + } + + @Nested + class TestBuildInvalidCheckTokenResponse { + + private final TokenValidationResult tokenCheckResult = TokenValidationResultTestFactory.createInvalid(); + + @BeforeEach + void mock() { + when(tokenCheckMapper.toCheckErrors(any())).thenReturn(GrpcCheckTokenResponseTestFactory.GRPC_CHECK_ERRORS); + } + + @Test + void shouldCallMapper() { + buildCheckTokenResponse(); + + verify(tokenCheckMapper).toCheckErrors(tokenCheckResult); + } + + @Test + void shouldReturnResponse() { + var response = buildCheckTokenResponse(); + + assertThat(response).isEqualTo(GrpcCheckTokenResponseTestFactory.createInvalid()); + } + + private GrpcCheckTokenResponse buildCheckTokenResponse() { + return service.buildInvalidCheckTokenResponse(tokenCheckResult); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckMapperTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckMapperTest.java deleted file mode 100644 index 3691e57a36a95411f67f3afd3404ba10804de2e7..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckMapperTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; -import org.mapstruct.factory.Mappers; - -class TokenCheckMapperTest { - private final TokenCheckMapper mapper = Mappers.getMapper(TokenCheckMapper.class); - - @Test - void shouldMapPostfachHandle() { - var res = mapper.toGrpcTokenResponse(TokenCheckResultTestFactory.create()); - - assertThat(res.getPostkorbHandle()).isEqualTo(TokenCheckResultTestFactory.POSTKORB_HANDLE); - } - - @Test - void shouldMapTrustLevel() { - var res = mapper.toGrpcTokenResponse(TokenCheckResultTestFactory.create()); - - assertThat(res.getTrustLevel()).isEqualTo(TokenCheckResultTestFactory.TRUST_LEVEL); - } - - @Test - void shouldMapOtherFields() { - var res = mapper.toGrpcTokenResponse(TokenCheckResultTestFactory.create()); - - assertThat(res.getOtherFieldsList()).hasSize(2); - } - -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckPropertiesITCase.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckPropertiesITCase.java deleted file mode 100644 index 661af9e896ac73ddb669e0af80274044418a469e..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckPropertiesITCase.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.TestPropertySource; - -@SpringBootTest(classes = { TokenCheckTestConfiguration.class }) -class TokenCheckPropertiesITCase { - private static final String URL = "https=//infra-pre-id.bayernportal.de/idp"; - private static final String TRUST_LEVEL_KEY = "trustLevel"; - private static final String TRUST_LEVEL_VALUE = "urn=oid:1.2.3.4"; - private static final String POSTKORB_HANDLE_KEY = "postkorbHandle"; - private static final String POSTKORB_HANDLE_VALUE = "urn=oid:2.5.4.18"; - - @DisplayName("Test loading token checker configuration") - @Nested - @TestPropertySource(properties = { - TokenCheckProperties.PREFIX + ".entities[0].idpEntityId=" + TokenCheckPropertiesITCase.URL, - TokenCheckProperties.PREFIX + ".entities[0].key=classpath:test2-enc.key", - TokenCheckProperties.PREFIX + ".entities[0].certificate=classpath:test2-enc.crt", - TokenCheckProperties.PREFIX + ".entities[0].metadata=classpath:metadata/bayernid-idp-infra.xml", - TokenCheckProperties.PREFIX + ".entities[0].mappings.postkorbHandle=" + TokenCheckPropertiesITCase.POSTKORB_HANDLE_VALUE, - TokenCheckProperties.PREFIX + ".entities[0].mappings.trustLevel=" + TokenCheckPropertiesITCase.TRUST_LEVEL_VALUE - }) - class TestLoadingConfiguration { - @Autowired - private TokenCheckProperties tokenCheckProperties; - - @Nested - class TestInitialization { - @Test - void shouldHaveProperties() { - assertThat(tokenCheckProperties).isNotNull(); - } - - @Test - void shouldHaveEntities() { - assertThat(tokenCheckProperties.getEntities()).isNotNull(); - } - } - - @Nested - class TestLoadedEntity { - @Test - void shouldHaveIdpEntityId() { - assertThat(tokenCheckProperties.getEntities().getFirst().getIdpEntityId()).isEqualTo(URL); - } - - @Test - void shouldHaveDevEncKey() { - assertThat(tokenCheckProperties.getEntities().getFirst().getKey()).isNotNull(); - } - - @Test - void shouldHaveDevEncCrt() { - assertThat(tokenCheckProperties.getEntities().getFirst().getCertificate()).isNotNull(); - } - - @Test - void shouldHaveMetadata() { - assertThat(tokenCheckProperties.getEntities().getFirst().getMetadata()).isNotNull(); - } - - @Test - void shouldHavePostkorbHandleMapping() { - assertThat(tokenCheckProperties.getEntities().getFirst().getMappings()).containsEntry(POSTKORB_HANDLE_KEY, POSTKORB_HANDLE_VALUE); - } - - @Test - void shouldNotHaveUseIdAsPostkorbHandle() { - assertThat(tokenCheckProperties.getEntities().getFirst().getUseIdAsPostkorbHandle()).isFalse(); - } - - @Test - void shouldHaveConfiguredTrustLevelMapping() { - assertThat(tokenCheckProperties.getEntities().getFirst().getMappings()).containsEntry(TRUST_LEVEL_KEY, TRUST_LEVEL_VALUE); - } - } - } - - @DisplayName("Test loading mapping") - @TestPropertySource(properties = { - TokenCheckProperties.PREFIX + ".entities[0].idpEntityId=" + URL, - TokenCheckProperties.PREFIX + ".entities[0].key=classpath:test2-enc.key", - TokenCheckProperties.PREFIX + ".entities[0].certificate=classpath:test2-enc.crt", - TokenCheckProperties.PREFIX + ".entities[0].metadata=classpath:metadata/bayernid-idp-infra.xml", - TokenCheckProperties.PREFIX + ".entities[0]use-id-as-postkorb-handle=true", - TokenCheckProperties.PREFIX + ".entities[0].mappings.test=test2", - }) - @Nested - class TestLoadingMappingConfiguration { - @Autowired - private TokenCheckProperties tokenCheckProperties; - - @Test - void shouldHaveUseIdAsPostkorbHandle() { - assertThat(tokenCheckProperties.getEntities().getFirst().getUseIdAsPostkorbHandle()).isTrue(); - } - - @Test - void shouldHaveConfiguredTestMapping() { - assertThat(tokenCheckProperties.getEntities().getFirst().getMappings()).containsEntry("test", "test2"); - } - - @Test - void shouldNotHavePostkorbHandleMapping() { - assertThat(tokenCheckProperties.getEntities().getFirst().getMappings()).doesNotContain(entry(POSTKORB_HANDLE_KEY, - POSTKORB_HANDLE_VALUE)); - } - } -} \ No newline at end of file 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 c6620963c391b7e28c620c907f2fdab25eb44b18..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckServiceTest.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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 static de.ozgcloud.token.saml.SamlTokenTestUtils.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -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.SamlConfiguration; -import de.ozgcloud.token.saml.SamlConfigurationRegistry; -import de.ozgcloud.token.saml.SamlTokenTestUtils; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; - -@Disabled("need to refactor: The keyfile is empty") -class TokenCheckServiceTest { - final static String POSTKORB_HANDLE_BAYERN_ID = "pk-bayern"; - final static String POSTKORB_HANDLE_MUK = "pk-muk"; - final static String TRUST_LEVEL = "low"; - public static final String TRUST_LEVEL_NAME_BAYERN_ID = "urn:oid:1.2.40.0.10.2.1.1.261.94"; - public static final String POSTKORB_HANDLE_NAME_BAYERN_ID = "urn:oid:2.5.4.18"; - - @InjectMocks - private TokenCheckService tokenCheckerService; - - @Mock - private Saml2VerificationService verificationService; - - @Mock - private Saml2DecryptionService decryptionService; - - @Mock - private SamlConfigurationRegistry samlConfigurationRegistry; - - @Mock - private Saml2ParseService parseService; - - private String token; - - @Nested - class TestTokenCheckerService { - - @BeforeEach - void setup() throws ComponentInitializationException { - var response = initBayernIdSamlResponse(); - when(parseService.parse(anyString())).thenReturn(response); - - var config = SamlTokenTestUtils.initConfig(BAYERN_ID); - when(samlConfigurationRegistry.getConfiguration(IDP_ENTITY_ID_BAYERN_ID)).thenReturn( - config.getConfiguration(IDP_ENTITY_ID_BAYERN_ID)); - - token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - } - - private static @NotNull Response initBayernIdSamlResponse() { - Response response = mock(Response.class); - Issuer issuer = mock(Issuer.class); - when(issuer.getValue()).thenReturn(IDP_ENTITY_ID_BAYERN_ID); - when(response.getIssuer()).thenReturn(issuer); - return response; - } - - @Test - void shouldCheckToken() { - var result = tokenCheckerService.getTokenCheckResult(token); - - assertThat(result).isNotNull(); - } - - @Test - void shouldCallVerificationService() { - tokenCheckerService.checkToken(token); - - verify(verificationService).verify(anyString()); - } - - @Test - void shouldCallParser() { - tokenCheckerService.getTokenCheckResult(token); - - verify(parseService).parse(anyString()); - } - - @Test - void shouldDecryptAttributes() { - tokenCheckerService.getTokenCheckResult(token); - - verify(decryptionService).decryptAttributes(any(Response.class), any(SamlConfiguration.class)); - } - } - - @Nested - class TestDecryptingAttributes { - @Nested - class TestBayernId { - @BeforeEach - void setup() throws ComponentInitializationException { - var response = initMockSamlResponse(); - when(parseService.parse(anyString())).thenReturn(response); - - var config = SamlTokenTestUtils.initConfig(BAYERN_ID); - when(samlConfigurationRegistry.getConfiguration(IDP_ENTITY_ID_BAYERN_ID)).thenReturn( - config.getConfiguration(IDP_ENTITY_ID_BAYERN_ID)); - - token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - } - - private static @NotNull Response initMockSamlResponse() { - Response response = mock(Response.class); - Issuer issuer = mock(Issuer.class); - when(issuer.getValue()).thenReturn(IDP_ENTITY_ID_BAYERN_ID); - when(response.getIssuer()).thenReturn(issuer); - return response; - } - - @Test - void shouldGetPostfachHandleFromBayernIdToken() { - var attributes = List.of(new TokenAttribute(POSTKORB_HANDLE_NAME_BAYERN_ID, POSTKORB_HANDLE_BAYERN_ID)); - when(decryptionService.decryptAttributes(any(), any(SamlConfiguration.class))).thenReturn( - attributes); - - TokenCheckResult result = tokenCheckerService.getTokenCheckResult(token); - - assertThat(result.postkorbHandle()).isEqualTo(POSTKORB_HANDLE_BAYERN_ID); - } - - @Test - void shouldGetTrustLevel() { - var attributes = List.of(new TokenAttribute(TRUST_LEVEL_NAME_BAYERN_ID, TRUST_LEVEL)); - when(decryptionService.decryptAttributes(any(), any(SamlConfiguration.class))).thenReturn( - attributes); - - TokenCheckResult result = tokenCheckerService.getTokenCheckResult(token); - - assertThat(result.trustLevel()).isEqualTo(TRUST_LEVEL); - } - } - - @Nested - class TestMuk { - @BeforeEach - void setup() throws ComponentInitializationException { - var response = initMukSamlResponse(); - when(parseService.parse(anyString())).thenReturn(response); - - var config = SamlTokenTestUtils.initConfig(MUK); - when(samlConfigurationRegistry.getConfiguration(IDP_ENTITY_ID_MUK)).thenReturn(config.getConfiguration(IDP_ENTITY_ID_MUK)); - - token = TestUtils.loadTextFile("SamlResponseMuk.xml"); - } - - private static @NotNull Response initMukSamlResponse() { - Response response = mock(Response.class); - Issuer issuer = mock(Issuer.class); - when(issuer.getValue()).thenReturn(IDP_ENTITY_ID_MUK); - when(response.getIssuer()).thenReturn(issuer); - when(response.getID()).thenReturn(POSTKORB_HANDLE_MUK); - return response; - } - - @Test - void shouldGetPostfachHandleFromMukToken() { - TokenCheckResult result = tokenCheckerService.getTokenCheckResult(token); - - assertThat(result.postkorbHandle()).isEqualTo(POSTKORB_HANDLE_MUK); - } - } - } -} \ 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 deleted file mode 100644 index 7759cf602c9ce5f58f479a3f13c4207831696b2e..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenCheckTestConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.HashMap; - -import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -import de.ozgcloud.token.saml.SamlConfigurationRegistry; -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; - -@ConfigurationPropertiesScan("de.ozgcloud.token") -@EnableConfigurationProperties(TokenCheckProperties.class) -public class TokenCheckTestConfiguration { - @Bean - public ParserPool parserPool() throws ComponentInitializationException { - var localParserPool = new BasicParserPool(); - - final var features = SamlTokenUtils.createFeatureMap(); - localParserPool.setBuilderFeatures(features); - localParserPool.setBuilderAttributes(new HashMap<>()); - localParserPool.initialize(); - - return localParserPool; - } - - @Bean - public SamlConfigurationRegistry samlConfigurationRegistry() { - return new SamlConfigurationRegistry(); - } - -} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultMapperTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultMapperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d03b398bcbf640c870c6ef10513c7dd6dfe3bdc3 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultMapperTest.java @@ -0,0 +1,74 @@ +/* + * 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 org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; + +import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory; + +class TokenValidationResultMapperTest { + + private final TokenValidationResultMapper mapper = spy(Mappers.getMapper(TokenValidationResultMapper.class)); + + @Nested + class TestToTokenAttributes { + + @Test + void shouldMapTokenAttributes() { + var result = mapper.toTokenAttributes(TokenValidationResultTestFactory.createValid()); + + assertThat(result).usingRecursiveComparison().isEqualTo(GrpcTokenAttributesTestFactory.create()); + } + } + + @Nested + class TestToCheckErrors { + + @BeforeEach + void init() { + doReturn(GrpcCheckErrorTestFactory.create()).when(mapper).toCheckError(any()); + } + + @Test + void shouldMapCheckErrors() { + var result = mapper.toCheckErrors(TokenValidationResultTestFactory.createInvalid()); + + assertThat(result).usingRecursiveComparison().isEqualTo(GrpcCheckErrorsTestFactory.create()); + } + } + + @Nested + class TestToCheckError { + + @Test + void shouldMapCheckError() { + var result = mapper.toCheckError(ValidationErrorTestFactory.create()); + + assertThat(result).usingRecursiveComparison().isEqualTo(GrpcCheckErrorTestFactory.create()); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..6d0d98c85de2c4445d9fd1dd8d4306a955794329 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenValidationResultTestFactory.java @@ -0,0 +1,53 @@ +/* + * 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 de.ozgcloud.token.TokenValidationResult.TokenValidationResultBuilder; +import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory; +import de.ozgcloud.token.common.errorhandling.ValidationError; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +public class TokenValidationResultTestFactory { + + public static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create(); + public static final ValidationError VALIDATION_ERROR = ValidationErrorTestFactory.create(); + + public static TokenValidationResult createValid() { + return createValidBuilder().valid(true).build(); + } + + public static TokenValidationResultBuilder createValidBuilder() { + return TokenValidationResult.builder() + .valid(true) + .attributes(TOKEN_ATTRIBUTES); + } + + public static TokenValidationResult createInvalid() { + return createInvalidBuilder().build(); + } + + public static TokenValidationResult.TokenValidationResultBuilder createInvalidBuilder() { + return TokenValidationResult.builder() + .valid(false) + .validationError(VALIDATION_ERROR); + } +} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6142e27ad446eb0c7bec7997333b189cc4fbec9f --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/common/CallContextGrpcServerInterceptorTest.java @@ -0,0 +1,201 @@ +/* + * 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.common; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; +import java.util.UUID; + +import org.apache.logging.log4j.CloseableThreadContext; +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.springframework.test.util.ReflectionTestUtils; + +import de.ozgcloud.common.grpc.GrpcUtil; +import io.grpc.Metadata; +import io.grpc.ServerCall; + +class CallContextGrpcServerInterceptorTest { + + @InjectMocks + private CallContextGrpcServerInterceptor interceptor; + + @Mock + private ServerCall.Listener<RequestTest> callListener; + @Mock + private Metadata metadata; + + private CallContextGrpcServerInterceptor.LogContextSettingListener<RequestTest> listener; + + @BeforeEach + void init() { + listener = spy(interceptor.new LogContextSettingListener(callListener, metadata)); + } + + @Nested + class TestGetRequestId { + + @Test + void shouldCallGetRequestId() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + listener.getRequestId(metadata); + + grpcUtilMock.verify(() -> GrpcUtil.getRequestId(metadata)); + } + } + + @Test + void shouldGetRequestIdFromMetadata() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + var requestId = UUID.randomUUID().toString(); + grpcUtilMock.when(() -> GrpcUtil.getRequestId(any())).thenReturn(Optional.of(requestId)); + + var result = listener.getRequestId(metadata); + + assertThat(result).isEqualTo(requestId); + } + } + + @Test + void shouldReturnGeneratedId() { + try (var grpcUtilMock = mockStatic(GrpcUtil.class)) { + grpcUtilMock.when(() -> GrpcUtil.getRequestId(any())).thenReturn(Optional.empty()); + + var result = listener.getRequestId(metadata); + + assertThat(result).isNotEmpty(); + } + } + } + + @Nested + class TestOnMessage { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onMessage(new RequestTest()); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnHalfClose { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onHalfClose(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnCancel { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onCancel(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnComplete { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onComplete(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestOnReady { + + @Test + void shouldCallDoSurroundOn() { + doNothing().when(listener).doSurroundOn(any()); + + listener.onReady(); + + verify(listener).doSurroundOn(any()); + } + } + + @Nested + class TestDoSurroundOn { + + @Mock + private Runnable runnable; + + private String requestId; + + @BeforeEach + void init() { + requestId = (String) ReflectionTestUtils.getField(listener, "requestId"); + } + + @Test + void shouldSetThreadContext() { + try (var contextMock = mockStatic(CloseableThreadContext.class)) { + doSurroundOn(); + + contextMock.verify(() -> CloseableThreadContext.put(GrpcUtil.KEY_REQUEST_ID, requestId)); + } + } + + @Test + void shouldExecuteRunnable() { + doSurroundOn(); + + verify(runnable).run(); + } + + private void doSurroundOn() { + listener.doSurroundOn(runnable); + } + } + + private record RequestTest() { + } + + private record ResponseTest() { + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckResponseTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/common/errorHandler/ValidationErrorTestFactory.java similarity index 65% rename from token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckResponseTestFactory.java rename to token-checker-server/src/test/java/de/ozgcloud/token/common/errorHandler/ValidationErrorTestFactory.java index c8db5d75f54514bcae2ff0163ab385ffbcf87c11..bdb3374e1e3b8c9901b8717c0b3bca39deddf871 100644 --- a/token-checker-server/src/test/java/de/ozgcloud/token/GrpcTokenCheckResponseTestFactory.java +++ b/token-checker-server/src/test/java/de/ozgcloud/token/common/errorHandler/ValidationErrorTestFactory.java @@ -21,25 +21,25 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -package de.ozgcloud.token; -import java.util.UUID; +package de.ozgcloud.token.common.errorHandler; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import com.thedeanda.lorem.LoremIpsum; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -class GrpcTokenCheckResponseTestFactory { - static final String POSTKORN_HANDLE = UUID.randomUUID().toString(); - static final String TRUST_LEVEL = "LOW"; +import de.ozgcloud.token.common.errorhandling.ValidationError; - static GrpcTokenCheckResponse create() { +public class ValidationErrorTestFactory { + + public static final String ERROR_MESSAGE = LoremIpsum.getInstance().getWords(4); + public static final Exception CAUSE = new Exception(); + + public static ValidationError create() { return createBuilder().build(); } - static GrpcTokenCheckResponse.Builder createBuilder() { - return GrpcTokenCheckResponse.newBuilder() - .setPostkorbHandle(POSTKORN_HANDLE) - .setTrustLevel(TRUST_LEVEL); + public static ValidationError.ValidationErrorBuilder createBuilder() { + return ValidationError.builder() + .message(ERROR_MESSAGE) + .cause(CAUSE); } } diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2DecryptionServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2DecryptionServiceTest.java deleted file mode 100644 index 4e83489b00684a954c243dbd54798b5fbe480e94..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2DecryptionServiceTest.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 de.ozgcloud.token.saml.SamlTokenTestUtils.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -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.Attribute; -import org.opensaml.saml.saml2.core.AttributeStatement; -import org.opensaml.saml.saml2.core.Response; -import org.springframework.security.saml2.Saml2Exception; - -import de.ozgcloud.common.test.TestUtils; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; - -@Disabled("need to refactor: The keyfile is empty") -class Saml2DecryptionServiceTest { - private Response samlResponse; - - @Spy - private Saml2DecryptionService saml2DecryptionService; - - private SamlConfiguration configuration; - private Saml2ParseService parseService; - - @BeforeEach - void setup() throws ComponentInitializationException { - var parserPool = SamlTokenTestUtils.initParserPool(); - - var samlConfigurationRegistry = SamlTokenTestUtils.initConfig(BAYERN_ID); - configuration = samlConfigurationRegistry.getConfiguration(IDP_ENTITY_ID_BAYERN_ID); - parseService = new Saml2ParseService(parserPool); - var token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - samlResponse = parseService.parse(token); - } - - @Test - void shouldDecryptResponse() { - var res = saml2DecryptionService.decryptAttributes(samlResponse, configuration); - - assertThat(res).isNotNull(); - } - - @Test - void shouldDecrypt() { - assertThat(samlResponse.getAssertions()).isEmpty(); - - saml2DecryptionService.decryptResponseElements(samlResponse, configuration.decrypter()); - - assertThat(samlResponse.getAssertions()).isNotEmpty(); - } - - @Test - void shouldThrowExceptionWhenDecryptionFails() { - var token = TestUtils.loadTextFile("BrokenSamlResponse.xml"); - var decrypter = configuration.decrypter(); - - samlResponse = parseService.parse(token); - - assertThatExceptionOfType(Saml2Exception.class).isThrownBy( - () -> saml2DecryptionService.decryptResponseElements(samlResponse, decrypter)); - } - - @Test - void shouldHaveSubject() { - saml2DecryptionService.decryptResponseElements(samlResponse, configuration.decrypter()); - var samlAssertion = samlResponse.getAssertions().getFirst(); - - assertThat(samlAssertion.getSubject()).isNotNull(); - } - - @Test - void shouldHaveAuthnStatements() { - saml2DecryptionService.decryptResponseElements(samlResponse, configuration.decrypter()); - var samlAssertion = samlResponse.getAssertions().getFirst(); - var authnStatements = samlAssertion.getAuthnStatements(); - - assertThat(authnStatements).isNotNull(); - } - - @Test - void shouldHaveStatements() { - saml2DecryptionService.decryptResponseElements(samlResponse, configuration.decrypter()); - var samlAssertion = samlResponse.getAssertions().getFirst(); - var statements = samlAssertion.getStatements(); - - assertThat(statements).isNotNull(); - } - - @Test - void shouldHaveAttributes() { - saml2DecryptionService.decryptResponseElements(samlResponse, configuration.decrypter()); - var samlAssertion = samlResponse.getAssertions().getFirst(); - var statements = (AttributeStatement) samlAssertion.getStatements().get(1); - var attributes = statements.getAttributes(); - assertThat(attributes).hasSize(7); - } - - @Test - void shouldGetXSStringAttribute() { - var attributeValue = mock(XSString.class); - when(attributeValue.getValue()).thenReturn("test"); - - var value = saml2DecryptionService.getAttributeValue(attributeValue); - - assertThat(value).isEqualTo("test"); - } - - @Test - void shouldGetXSAnyAttribute() { - var attributeValue = mock(XSAny.class); - when(attributeValue.getTextContent()).thenReturn("test"); - - var value = saml2DecryptionService.getAttributeValue(attributeValue); - - assertThat(value).isEqualTo("test"); - } - - @Test - void shouldGetXSIntegerAttribute() { - var attributeValue = mock(XSInteger.class); - when(attributeValue.getValue()).thenReturn(1); - - var value = saml2DecryptionService.getAttributeValue(attributeValue); - - assertThat(value).isEqualTo("1"); - } - - @Test - void shouldGetXSBooleanAttribute() { - var attributeValue = mock(XSBoolean.class); - when(attributeValue.getValue()).thenReturn(XSBooleanValue.valueOf("true")); - - var value = saml2DecryptionService.getAttributeValue(attributeValue); - - assertThat(value).isEqualTo("true"); - } - - @Test - void shouldGetUnknownAttribute() { - var value = saml2DecryptionService.getAttributeValue(mock(XMLObject.class)); - - assertThat(value).startsWith("Mock for XMLObject"); - } - - @Test - void shouldGetAttributes() { - var attribute = mock(Attribute.class); - - var attributeValue1 = mock(XSInteger.class); - when(attributeValue1.getValue()).thenReturn(1); - var attributeValue2 = mock(XSString.class); - when(attributeValue2.getValue()).thenReturn("test"); - when(attribute.getAttributeValues()).thenReturn(List.of(attributeValue1, attributeValue2)); - - var value = saml2DecryptionService.getAttributeValues(attribute); - - assertThat(value).isEqualTo("1;test"); - } -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2ParseServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2ParseServiceTest.java deleted file mode 100644 index 07f5115078b06d0e68b89bd1268be199895a361b..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2ParseServiceTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 de.ozgcloud.token.saml.SamlTokenTestUtils.*; -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Disabled; -import org.opensaml.saml.saml2.core.Response; - -import de.ozgcloud.common.test.TestUtils; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; - -@Disabled("need to refactor: The keyfile is empty") -class Saml2ParseServiceTest { - private Saml2ParseService parser; - - @BeforeEach - void setup() throws ComponentInitializationException { - var parserPool = SamlTokenTestUtils.initParserPool(); - - SamlTokenTestUtils.initConfig(BAYERN_ID); - - parser = new Saml2ParseService(parserPool); - } - - @Test - void shouldInit() { - assertThat(parser).isNotNull(); - } - - @Test - void shouldParseSamlToken() { - var response = getResponse(); - assertThat(response).isNotNull(); - } - - @Test - void shouldHaveAssertions() { - var response = getResponse(); - assertThat(response.getAssertions()).isNotNull(); - } - - @Test - void shouldHaveEncryptedAssertions() { - var response = getResponse(); - assertThat(response.getEncryptedAssertions()).isNotNull(); - } - - @Test - void shouldHaveIssuer() { - var response = getResponse(); - assertThat(response.getIssuer().getValue()).isEqualTo(IDP_ENTITY_ID_BAYERN_ID); - } - - @Test - void shouldGetXMLObject() throws Exception { - try (var tokenStream = TestUtils.loadFile("SamlResponseBayernId.xml")) { - assertThat(parser.xmlObject(tokenStream)).isNotNull(); - } - } - - private Response getResponse() { - var token = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - - return parser.parse(token); - } - -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2VerificationServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2VerificationServiceTest.java deleted file mode 100644 index 1023ff97c9a30620fd783ac759f875b5a1febc2c..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/Saml2VerificationServiceTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 de.ozgcloud.token.saml.Saml2VerificationService.*; -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import de.ozgcloud.common.test.TestUtils; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; - -@Disabled("need to refactor: The keyfile is empty") -class Saml2VerificationServiceTest { - private Saml2VerificationService verifier; - - @BeforeEach - void setup() throws ComponentInitializationException { - var parserPool = SamlTokenTestUtils.initParserPool(); - - var samlConfigurationRegistry = SamlTokenTestUtils.initConfig(SamlTokenTestUtils.BAYERN_ID); - - var parser = new Saml2ParseService(parserPool); - - verifier = new Saml2VerificationService(parser, samlConfigurationRegistry); - } - - @Test - void shouldGetVerificationErrorInvalidSignature() { - var samlResponse = TestUtils.loadTextFile("SamlResponseBayernId.xml"); - - var res = verifier.verify(samlResponse); - - assertThat(res.getFirst().getErrorCode()).isEqualTo("invalid_signature"); - assertThat(res.getFirst().getDescription()).startsWith(INVALID_SIGNATURE); - } - - @Test - void shouldGetVerificationErrorNoSignature() { - var samlResponse = TestUtils.loadTextFile("BrokenSamlResponseNoSignature.xml"); - - var res = verifier.verify(samlResponse); - - assertThat(res.getFirst().getErrorCode()).isEqualTo("invalid_signature"); - assertThat(res.getFirst().getDescription()).isEqualTo(SIGNATURE_MISSING); - } - - @Test - void shouldGetVerificationErrorNoReference() { - var samlResponse = TestUtils.loadTextFile("BrokenSamlResponseNoReference.xml"); - - var res = verifier.verify(samlResponse); - - assertThat(res.getFirst().getErrorCode()).isEqualTo("invalid_signature"); - assertThat(res.getFirst().getDescription()).startsWith(INVALID_SIGNATURE_PROFILE); - } -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlAttributeServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlAttributeServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ca2d61ac64384215c7d05a7af64f74f3165d358f --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlAttributeServiceTest.java @@ -0,0 +1,816 @@ +/* + * 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.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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.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 com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.token.TokenAttributeTestFactory; +import de.ozgcloud.token.TokenAttributes; +import de.ozgcloud.token.TokenAttributesTestFactory; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import de.ozgcloud.token.common.errorhandling.ValidationError; +import lombok.SneakyThrows; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; + +class SamlAttributeServiceTest { + + @Spy + @InjectMocks + private SamlAttributeService service; + + @Mock + private SignatureTrustEngine signatureTrustEngine; + @Mock + private Decrypter decrypter; + @Mock + private SAMLSignatureProfileValidator profileValidator; + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private CriteriaSet verificationCriteria; + + @Nested + class TestGetAttributes { + + private static final Map<String, String> TOKEN_ATTRIBUTES_MAP = Map.of("key", "value"); + + @Mock + private Response token; + @Mock + private TokenAttributes tokenAttributes; + + @BeforeEach + void init() { + doNothing().when(service).validateToken(any()); + doReturn(TOKEN_ATTRIBUTES_MAP).when(service).decryptSamlAttributes(any()); + doReturn(tokenAttributes).when(service).buildTokenAttributes(any(), any()); + } + + @Test + void shouldCallValidateToken() { + getAttributes(); + + verify(service).validateToken(token); + } + + @Test + void shouldCallDecryptAttributes() { + getAttributes(); + + verify(service).decryptSamlAttributes(token); + } + + @Test + void shouldCallBuildTokenAttributes() { + getAttributes(); + + verify(service).buildTokenAttributes(TOKEN_ATTRIBUTES_MAP, token); + } + + @Test + void shouldReturnResult() { + var result = getAttributes(); + + assertThat(result).isSameAs(tokenAttributes); + } + + private TokenAttributes getAttributes() { + return service.getAttributes(token); + } + } + + @Nested + class TestValidateToken { + + @Mock + private Response token; + + @Test + void shouldThrowExceptionWhenMissingSignature() { + assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken); + } + + @Nested + class TestValidateSuccessfully { + + @Mock + private Signature signature; + + @BeforeEach + void init() { + when(token.getSignature()).thenReturn(signature); + doReturn(Optional.empty()).when(service).validateSignatureProfile(any()); + doReturn(Optional.empty()).when(service).validateSignature(any()); + } + + @Test + void shouldNotThrowException() { + assertDoesNotThrow(TestValidateToken.this::validateToken); + } + + @Test + void shouldCallValidateSignatureProfile() { + validateToken(); + + verify(service).validateSignatureProfile(signature); + } + + @Test + void shouldCallValidateSignature() { + validateToken(); + + verify(service).validateSignature(signature); + } + } + + @Nested + class TestValidationFailed { + + @Mock + private Signature signature; + @Mock + private ValidationError signatureProfileError; + @Mock + private ValidationError signatureError; + + @BeforeEach + void init() { + when(token.getSignature()).thenReturn(signature); + doReturn(Optional.of(signatureProfileError)).when(service).validateSignatureProfile(any()); + doReturn(Optional.of(signatureError)).when(service).validateSignature(any()); + } + + @Test + void shouldAddInvalidSignatureProfileToException() { + var exception = assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken); + + assertThat(exception.getValidationErrors()).contains(signatureProfileError); + } + + @Test + void shouldAddInvalidSignatureToException() { + var exception = assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken); + + assertThat(exception.getValidationErrors()).contains(signatureError); + } + } + + private void validateToken() { + service.validateToken(token); + } + } + + @Nested + class TestValidateSignatureProfile { + + @Mock + private Signature signature; + + @Nested + class TestSignatureProfileValide { + + @Test + @SneakyThrows + void shouldCallProfileValidator() { + service.validateSignatureProfile(signature); + + verify(profileValidator).validate(signature); + } + + @Test + void shouldReturnEmpty() { + var result = service.validateSignatureProfile(signature); + + assertThat(result).isEmpty(); + } + } + + @Nested + class TestInvalidSignatureProfile { + + private static final String EXCEPTION_MESSAGE = LoremIpsum.getInstance().getWords(1); + + private final SignatureException signatureException = new SignatureException(EXCEPTION_MESSAGE); + + @BeforeEach + @SneakyThrows + void init() { + doThrow(signatureException).when(profileValidator).validate(any()); + } + + @Test + void shouldSetMessage() { + var result = service.validateSignatureProfile(signature); + + assertThat(result).get().extracting(ValidationError::getMessage, STRING) + .hasSizeGreaterThan(EXCEPTION_MESSAGE.length()) + .endsWith(EXCEPTION_MESSAGE); + } + + @Test + void shouldSetCause() { + var result = service.validateSignatureProfile(signature); + + assertThat(result).get().extracting(ValidationError::getCause).isEqualTo(signatureException); + } + } + } + + @Nested + class TestValidateSignature { + + @Mock + private Signature signature; + + @Test + @SneakyThrows + void shouldCallSignatureTrustEngine() { + service.validateSignature(signature); + + verify(signatureTrustEngine).validate(signature, verificationCriteria); + } + + @Test + @SneakyThrows + void shouldReturnEmpty() { + doReturn(true).when(signatureTrustEngine).validate(any(), any()); + + var result = service.validateSignature(signature); + + assertThat(result).isEmpty(); + } + + @SneakyThrows + @Test + void shouldSetMessageIfInvalid() { + doReturn(false).when(signatureTrustEngine).validate(any(), any()); + + var result = service.validateSignature(signature); + + assertThat(result).get().extracting(ValidationError::getMessage, STRING).isNotEmpty(); + } + + @Nested + class TestOnException { + + private static final String EXCEPTION_MESSAGE = LoremIpsum.getInstance().getWords(1); + + private final SecurityException signatureException = new SecurityException(EXCEPTION_MESSAGE); + + @BeforeEach + @SneakyThrows + void init() { + doThrow(signatureException).when(signatureTrustEngine).validate(any(), any()); + } + + @Test + void shouldSetMessage() { + var result = service.validateSignature(signature); + + assertThat(result).get().extracting(ValidationError::getMessage, STRING) + .hasSizeGreaterThan(EXCEPTION_MESSAGE.length()) + .endsWith(EXCEPTION_MESSAGE); + } + + @Test + void shouldSetCause() { + var result = service.validateSignature(signature); + + assertThat(result).get().extracting(ValidationError::getCause).isEqualTo(signatureException); + } + } + } + + @Nested + class TestDecryptSamlAttributes { + + @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() { + decryptSamlAttributes(); + + verify(service).decryptAssertion(encryptedAssertion); + } + + @Test + void shouldCallGetAttributeStatement() { + decryptSamlAttributes(); + + verify(service).getAttributeStatement(assertion); + } + + @Test + void shouldCallGetAttributeValues() { + decryptSamlAttributes(); + + verify(service).getAttributeValues(attribute); + } + + @Test + void shouldReturnAttributes() { + var result = decryptSamlAttributes(); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.asMap()); + } + + private Map<String, String> decryptSamlAttributes() { + return service.decryptSamlAttributes(token); + } + } + + @Nested + class TestDecryptAssertion { + + @Mock + private EncryptedAssertion encryptedAssertion; + @Mock + private Assertion assertion; + + @Test + @SneakyThrows + void shouldDecryptAssertion() { + when(decrypter.decrypt(any(EncryptedAssertion.class))).thenReturn(assertion); + + service.decryptAssertion(encryptedAssertion); + + verify(decrypter).decrypt(encryptedAssertion); + } + + @Test + @SneakyThrows + void shouldReturnResult() { + when(decrypter.decrypt(any(EncryptedAssertion.class))).thenReturn(assertion); + + var result = service.decryptAssertion(encryptedAssertion); + + assertThat(result).isEqualTo(assertion); + } + + @SneakyThrows + @Test + void shouldThrowException() { + when(decrypter.decrypt(any(EncryptedAssertion.class))).thenThrow(DecryptionException.class); + + assertThrows(TokenVerificationException.class, () -> service.decryptAssertion(encryptedAssertion)); + } + } + + @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)); + } + + @DisplayName("should return value of XSBoolean when") + @ParameterizedTest(name = "value is {0}") + @ValueSource(strings = { "true", "false" }) + void shouldReturnWhenXSBoolean(String value) { + XMLObject attrValue = when(mock(XSBoolean.class).getValue()).thenReturn(XSBooleanValue.valueOf(value)).getMock(); + + var result = service.getAttributeValue(attrValue); + + assertThat(result).isEqualTo(value); + } + } + + @Nested + class TestBuildTokenAttributes { + + private static final Map<String, String> TOKEN_ATTRIBUTES_MAP = TokenAttributeTestFactory.asMap(); + + @Mock + private Response token; + + @BeforeEach + void init() { + doReturn(true).when(service).isNotNamedAttribute(any()); + doReturn(TokenAttributesTestFactory.OTHER_ATTRIBUTE).when(service).buildTokenAttribute(any()); + } + + @Test + void shouldCallGetPostfachId() { + buildTokenAttributes(); + + verify(service).getPostfachId(TOKEN_ATTRIBUTES_MAP, token); + } + + @Test + void shouldSetPostfachId() { + doReturn(TokenAttributesTestFactory.POSTFACH_ID).when(service).getPostfachId(any(), any()); + + var result = buildTokenAttributes(); + + assertThat(result.getPostfachId()).isEqualTo(TokenAttributesTestFactory.POSTFACH_ID); + } + + @Test + void shouldCallGetTrustLevel() { + buildTokenAttributes(); + + verify(service).getTrustLevel(TOKEN_ATTRIBUTES_MAP); + } + + @Test + void shouldSetTrustLevel() { + doReturn(TokenAttributesTestFactory.TRUST_LEVEL).when(service).getTrustLevel(any()); + + var result = buildTokenAttributes(); + + assertThat(result.getTrustLevel()).isEqualTo(TokenAttributesTestFactory.TRUST_LEVEL); + } + + @Test + void shouldCallIsNotMappedField() { + buildTokenAttributes(); + + verify(service).isNotNamedAttribute(Map.entry(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE)); + } + + @Test + void shouldCallBuildTokenAttribute() { + buildTokenAttributes(); + + verify(service).buildTokenAttribute(Map.entry(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE)); + } + + @Test + void shouldSetOtherAttributes() { + var result = buildTokenAttributes(); + + assertThat(result.getOtherAttributes()).containsExactly(TokenAttributesTestFactory.OTHER_ATTRIBUTE); + } + + private TokenAttributes buildTokenAttributes() { + return service.buildTokenAttributes(TOKEN_ATTRIBUTES_MAP, token); + } + } + + @Nested + class TestGetPostfachId { + + @Mock + private Response token; + + @Nested + class TestTokenId { + + private static final String TOKEN_ID = UUID.randomUUID().toString(); + + @BeforeEach + void init() { + when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(true); + doReturn(TOKEN_ID).when(token).getID(); + } + + @Test + void shouldCallGetTokenId() { + getPostfachId(); + + verify(service).getPostfachId(TokenAttributeTestFactory.asMap(), token); + } + + @Test + void shouldReturnTokenId() { + var result = getPostfachId(); + + assertThat(result).contains(TOKEN_ID); + } + } + + @Nested + class TestTokenAttribute { + + @BeforeEach + void init() { + when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(false); + doReturn(TokenAttributeTestFactory.NAME).when(service).getPostfachIdKey(); + } + + @Test + void shouldCallGetMappedValue() { + getPostfachId(); + + verify(service).getPostfachIdKey(); + } + + @Test + void shouldReturnMappedValue() { + var result = getPostfachId(); + + assertThat(result).contains(TokenAttributeTestFactory.VALUE); + } + } + + private String getPostfachId() { + return service.getPostfachId(TokenAttributeTestFactory.asMap(), token); + } + } + + @Nested + class TestGetTrustLevel { + + @BeforeEach + void init() { + doReturn(TokenAttributeTestFactory.NAME).when(service).getTrustLevelKey(); + } + + @Test + void shouldCallGetMappedValue() { + getTrustLevel(); + + verify(service).getTrustLevelKey(); + } + + @Test + void shouldReturnMappedValue() { + var result = getTrustLevel(); + + assertThat(result).contains(TokenAttributeTestFactory.VALUE); + } + + private String getTrustLevel() { + return service.getTrustLevel(TokenAttributeTestFactory.asMap()); + } + } + + @Nested + class TestIsNotNamedAttribute { + + private static final String KEY = UUID.randomUUID().toString(); + + @Test + void shouldReturnTrueWhenNotMapped() { + var random = new Random(); + doReturn("postfachIdKey-" + random.nextInt()).when(service).getPostfachIdKey(); + doReturn("trustLevelKey-" + random.nextInt()).when(service).getTrustLevelKey(); + + var result = isNotMappedField(); + + assertThat(result).isTrue(); + } + + @Test + void shouldReturnFalseWhenMapped() { + doReturn(KEY).when(service).getPostfachIdKey(); + + var result = isNotMappedField(); + + assertThat(result).isFalse(); + } + + private boolean isNotMappedField() { + return service.isNotNamedAttribute(Map.entry(KEY, TokenAttributeTestFactory.VALUE)); + } + } + + @Nested + class TestGetPostfachIdKey { + + @Test + void shouldReturnDefaultKey() { + var result = service.getPostfachIdKey(); + + assertThat(result).isEqualTo(TokenAttributes.POSTFACH_ID_KEY); + } + + @Test + void shouldReturnMappedKey() { + when(tokenValidationProperty.getMappings()).thenReturn(Map.of(TokenAttributes.POSTFACH_ID_KEY, TokenAttributeTestFactory.NAME)); + + var result = service.getPostfachIdKey(); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.NAME); + } + } + + @Nested + class TestGetTrustLevelKey { + + @Test + void shouldReturnDefaultKey() { + var result = service.getTrustLevelKey(); + + assertThat(result).isEqualTo(TokenAttributes.TRUST_LEVEL_KEY); + } + + @Test + void shouldReturnMappedKey() { + when(tokenValidationProperty.getMappings()).thenReturn(Map.of(TokenAttributes.TRUST_LEVEL_KEY, TokenAttributeTestFactory.NAME)); + + var result = service.getTrustLevelKey(); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.NAME); + } + } + + @Nested + class TestBuildTokenAttribute { + + @Test + void shouldBuildTokenAttribute() { + var result = service.buildTokenAttribute(Map.entry(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE)); + + assertThat(result).usingRecursiveComparison().isEqualTo(TokenAttributeTestFactory.create()); + } + } + + @Nested + class TestGetMappedKey { + + @Test + void shouldReturnDefaultKey() { + var result = service.getMappedKey(TokenAttributeTestFactory.VALUE); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.VALUE); + } + + @Test + void shouldReturnMappedKey() { + when(tokenValidationProperty.getMappings()).thenReturn(Map.of(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE)); + + var result = service.getMappedKey(TokenAttributeTestFactory.VALUE); + + assertThat(result).isEqualTo(TokenAttributeTestFactory.NAME); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationRegistryTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationRegistryTest.java deleted file mode 100644 index a09316e65a79b03d23e024864274e690f8b1f9b6..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationRegistryTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.Mockito.*; - -import java.util.Map; - -import org.junit.jupiter.api.Test; -import org.opensaml.saml.saml2.encryption.Decrypter; -import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; - -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; - -class SamlConfigurationRegistryTest { - public static final String TEST_IDP = "test.idp"; - SamlConfigurationRegistry registry = new SamlConfigurationRegistry(); - - @Test - void shouldAddConfiguration() { - registry.addConfiguration(TEST_IDP, new SamlConfiguration( - mock(SignatureTrustEngine.class), mock(CriteriaSet.class), mock(Decrypter.class), Map.of(), false) - ); - - assertThat(registry.getConfiguration(TEST_IDP)).isNotNull(); - } - - @Test - void shouldThrowExceptionWhenEmptySignatureTrustEngine() { - assertThatException().isThrownBy(() -> registry.getConfiguration(TEST_IDP)); - } -} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..470c1f5ab5857f3484932f1d12c7fde24b9f2213 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlConfigurationTest.java @@ -0,0 +1,182 @@ +/* + * 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.Mockito.*; + +import java.util.List; + +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.encryption.Decrypter; +import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.token.TokenValidationProperties; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; + +class SamlConfigurationTest { + + private static final String IDP_ENTITY_ID = LoremIpsum.getInstance().getWords(1); + + @Spy + @InjectMocks + private SamlConfiguration configuration; + + @Mock + private SamlTrustEngineFactory samlTrustEngineFactory; + @Mock + private SamlDecrypterFactory samlDecrypterFactory; + + @Nested + class TestSamlServiceRegistry { + + @Mock + private TokenValidationProperties tokenValidationProperties; + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private SamlAttributeService tokenValidationService; + + @BeforeEach + void init() { + when(tokenValidationProperty.getIdpEntityId()).thenReturn(IDP_ENTITY_ID); + when(tokenValidationProperties.getEntities()).thenReturn(List.of(tokenValidationProperty)); + doReturn(tokenValidationService).when(configuration).samlAttributeService(any()); + } + + @Test + void shouldCallSamlTokenService() { + configuration.samlServiceRegistry(tokenValidationProperties); + + verify(configuration).samlAttributeService(tokenValidationProperty); + } + + @Test + void shouldAddService() { + var result = configuration.samlServiceRegistry(tokenValidationProperties); + + assertThat(result.getService(IDP_ENTITY_ID)).contains(tokenValidationService); + } + } + + @Nested + class TestSamlTokenService { + + @Mock + private TokenValidationProperty tokenValidationProperty; + @Mock + private SignatureTrustEngine signatureTrustEngine; + @Mock + private Decrypter decrypter; + @Mock + private SAMLSignatureProfileValidator profileValidator; + @Mock + private CriteriaSet verificationCriteria; + + @BeforeEach + void init() { + when(tokenValidationProperty.getIdpEntityId()).thenReturn(IDP_ENTITY_ID); + doReturn(signatureTrustEngine).when(samlTrustEngineFactory).buildSamlTrustEngine(any()); + doReturn(decrypter).when(samlDecrypterFactory).buildDecrypter(any()); + doReturn(profileValidator).when(configuration).samlSignatureProfileValidator(); + doReturn(verificationCriteria).when(configuration).buildVerificationCriteria(any()); + } + + @Test + void shouldCallBuildSamlTrustEngine() { + samlTokenService(); + + verify(samlTrustEngineFactory).buildSamlTrustEngine(tokenValidationProperty); + } + + @Test + void shouldSetSignatureTrustEngine() { + var result = samlTokenService(); + + assertThat(result).extracting("signatureTrustEngine").isEqualTo(signatureTrustEngine); + } + + @Test + void shouldCallBuildDecrypter() { + samlTokenService(); + + verify(samlDecrypterFactory).buildDecrypter(tokenValidationProperty); + } + + @Test + void shouldSetDecrypter() { + var result = samlTokenService(); + + assertThat(result).extracting("decrypter").isEqualTo(decrypter); + } + + @Test + void shouldCallSamlSignatureProfileValidator() { + samlTokenService(); + + verify(configuration).samlSignatureProfileValidator(); + } + + @Test + void shouldSetProfileValidator() { + var result = samlTokenService(); + + assertThat(result).extracting("profileValidator").isEqualTo(profileValidator); + } + + @Test + void shouldSetTokenValidationProperty() { + var result = samlTokenService(); + + assertThat(result).extracting("tokenValidationProperty").isEqualTo(tokenValidationProperty); + } + + @Test + void shouldCallBuildVerificationCriteria() { + samlTokenService(); + + verify(configuration).buildVerificationCriteria(IDP_ENTITY_ID); + } + + @Test + void shouldSetVerificationCriteria() { + var result = samlTokenService(); + + assertThat(result).extracting("verificationCriteria").isEqualTo(verificationCriteria); + } + + private SamlAttributeService samlTokenService() { + return configuration.samlAttributeService(tokenValidationProperty); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlDecrypterFactoryTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlDecrypterFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..60d558fc863fb2f1905f7b3131e750429f847518 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlDecrypterFactoryTest.java @@ -0,0 +1,192 @@ +/* + * 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.Mockito.*; + +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; + +import org.junit.jupiter.api.AfterEach; +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.MockedStatic; +import org.mockito.Spy; +import org.opensaml.saml.saml2.encryption.Decrypter; +import org.opensaml.security.credential.CredentialSupport; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.xmlsec.encryption.support.ChainingEncryptedKeyResolver; +import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; + +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import de.ozgcloud.token.saml.SamlDecrypterFactory.DecrypterBuilder; + +class SamlDecrypterFactoryTest { + + @Spy + @InjectMocks + private SamlDecrypterFactory factory; + + @Mock + private TokenValidationProperty tokenValidationProperty; + + @Nested + class TestBuildDecrypter { + + @Mock + private CollectionKeyInfoCredentialResolver keyInfoCredentialResolver; + @Mock + private ChainingEncryptedKeyResolver encryptedKeyResolver; + @Mock + private DecrypterBuilder decrypterBuilder; + @Mock + private Decrypter decrypter; + + private MockedStatic<DecrypterBuilder> decrypterBuilderMock; + + @BeforeEach + void init() { + doReturn(keyInfoCredentialResolver).when(factory).buildKeyInfoCredentialResolver(any()); + doReturn(encryptedKeyResolver).when(factory).buildEncryptedKeyResolver(); + decrypterBuilderMock = mockStatic(DecrypterBuilder.class); + decrypterBuilderMock.when(DecrypterBuilder::builder).thenReturn(decrypterBuilder); + when(decrypterBuilder.keyEncryptionKeyResolver(any())).thenReturn(decrypterBuilder); + when(decrypterBuilder.encryptedKeyElementsResolver(any())).thenReturn(decrypterBuilder); + when(decrypterBuilder.build()).thenReturn(decrypter); + } + + @AfterEach + void close() { + decrypterBuilderMock.close(); + } + + @Test + void shouldCallBuildKeyInfoCredentialResolver() { + buildDecrypter(); + + verify(factory).buildKeyInfoCredentialResolver(tokenValidationProperty); + } + + @Test + void shouldSetKeyEncryptionKeyResolver() { + buildDecrypter(); + + verify(decrypterBuilder).keyEncryptionKeyResolver(keyInfoCredentialResolver); + } + + @Test + void shouldCallBuildEncryptedKeyResolver() { + buildDecrypter(); + + verify(factory).buildEncryptedKeyResolver(); + } + + @Test + void shouldSetEncryptedKeyElementsResolver() { + buildDecrypter(); + + verify(decrypterBuilder).encryptedKeyElementsResolver(encryptedKeyResolver); + } + + @Test + void shouldCallBuild() { + buildDecrypter(); + + verify(decrypterBuilder).build(); + } + + @Test + void shouldReturnResult() { + var result = buildDecrypter(); + + assertThat(result).isSameAs(decrypter); + } + + private Decrypter buildDecrypter() { + return factory.buildDecrypter(tokenValidationProperty); + } + } + + @Nested + class TestBuildKeyInfoCredentialResolver { + + @Mock + private X509Certificate certificate; + @Mock + private RSAPrivateKey privateKey; + @Mock + private BasicX509Credential credential; + + private MockedStatic<CredentialSupport> credentialSupportMock; + + @BeforeEach + void init() { + doReturn(certificate).when(factory).getCertificate(any()); + doReturn(privateKey).when(factory).getPrivateKey(any()); + credentialSupportMock = mockStatic(CredentialSupport.class); + credentialSupportMock.when(() -> CredentialSupport.getSimpleCredential(any(X509Certificate.class), any())).thenReturn(credential); + } + + @AfterEach + void close() { + credentialSupportMock.close(); + } + + @Test + void shouldCallGetCertificate() { + buildKeyInfoCredentialResolver(); + + verify(factory).getCertificate(tokenValidationProperty); + } + + @Test + void shouldCallGetPrivateKey() { + buildKeyInfoCredentialResolver(); + + verify(factory).getPrivateKey(tokenValidationProperty); + } + + @Test + void shouldCallGetSimpleCredential() { + buildKeyInfoCredentialResolver(); + + credentialSupportMock.verify(() -> CredentialSupport.getSimpleCredential(certificate, privateKey)); + } + + @Test + void shouldReturnResult() { + var result = buildKeyInfoCredentialResolver(); + + assertThat(result.getCollection()).contains(credential); + } + + private CollectionKeyInfoCredentialResolver buildKeyInfoCredentialResolver() { + return factory.buildKeyInfoCredentialResolver(tokenValidationProperty); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTestConfiguration.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTestConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..c18f62ef6ae77d474b70e02c0b3dd70a522ffb7d --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTestConfiguration.java @@ -0,0 +1,86 @@ +/* + * 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.mockito.Mockito.*; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +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.encryption.Decrypter; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import lombok.SneakyThrows; + +@Configuration +public class SamlTestConfiguration { + + public static final String POSTFACH_ID = UUID.randomUUID().toString(); + public static final String TRUST_LEVEL = "STORK-QAA-Level-1"; + + public static final Map<String, String> SAML_ATTRIBUTES_MAP = Map.of( + "urn:oid:1.2.40.0.10.2.1.1.261.94", TRUST_LEVEL, + "urn:oid:2.5.4.18", POSTFACH_ID, + "urn:oid:0.9.2342.19200300.100.1.3", "mail@mail.local", + "urn:oid:1.3.6.1.4.1.25484.494450.2", "Benutzername", + "urn:oid:2.5.4.42", "Ozg", + "urn:oid:2.5.4.4", "Mgm" + ); + + @SneakyThrows + @Bean + @Primary + SamlTrustEngineFactory samlTrustEngineFactory(SignatureTrustEngine signatureTrustEngine) { + return when(mock(SamlTrustEngineFactory.class).buildSamlTrustEngine(any())).thenReturn(signatureTrustEngine).getMock(); + } + + @SneakyThrows + @Bean + @Primary + SamlDecrypterFactory samlDecrypterFactory() { + var attributes = SAML_ATTRIBUTES_MAP.entrySet().stream().map(e -> initAttributeMock(e.getKey(), e.getValue())).toList(); + AttributeStatement attributeStatement = when(mock(AttributeStatement.class).getAttributes()).thenReturn(attributes).getMock(); + Assertion assertion = when(mock(Assertion.class).getStatements()).thenReturn(List.of(attributeStatement)).getMock(); + Decrypter decrypter = when(mock(Decrypter.class).decrypt(any(EncryptedAssertion.class))).thenReturn(assertion).getMock(); + return when(mock(SamlDecrypterFactory.class).buildDecrypter(any())).thenReturn(decrypter).getMock(); + } + + Attribute initAttributeMock(String name, String value) { + Attribute attribute = mock(Attribute.class); + when(attribute.getName()).thenReturn(name); + XSString attrValue = when(mock(XSString.class).getValue()).thenReturn(value).getMock(); + when(attribute.getAttributeValues()).thenReturn(List.of(attrValue)); + return attribute; + } + +} \ No newline at end of file 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..06f05f1faf49401696df964d9ee3026c32947ab5 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenServiceTest.java @@ -0,0 +1,440 @@ +/* + * 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.Mockito.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Optional; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +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.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenAttributes; +import de.ozgcloud.token.TokenAttributesTestFactory; +import de.ozgcloud.token.TokenValidationResult; +import de.ozgcloud.token.TokenValidationResultTestFactory; +import de.ozgcloud.token.common.errorHandler.ValidationErrorTestFactory; +import de.ozgcloud.token.common.errorhandling.TokenVerificationException; +import lombok.SneakyThrows; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; + +class SamlTokenServiceTest { + + @Spy + @InjectMocks + private SamlTokenService service; + + @Mock + private SamlServiceRegistry samlServiceRegistry; + @Mock + private ParserPool parserPool; + @Mock + private ResponseUnmarshaller responseUnmarshaller; + + @Nested + class TestValidate { + + private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7); + + @Mock + private Response parsedToken; + + @Nested + class TestValid { + + private static final TokenValidationResult VALIDATION_RESULT = TokenValidationResultTestFactory.createValid(); + private static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create(); + + @BeforeEach + void init() { + doReturn(parsedToken).when(service).parseToken(anyString()); + doReturn(TOKEN_ATTRIBUTES).when(service).getAttributes(any()); + doReturn(VALIDATION_RESULT).when(service).buildValidTokenResult(any()); + } + + @Test + void shouldCallParseToken() { + validate(); + + verify(service).validate(SAML_TOKEN); + } + + @Test + void shouldCallGetAttributes() { + validate(); + + verify(service).getAttributes(parsedToken); + } + + @Test + void shouldCallBuildValidTokenResult() { + validate(); + + verify(service).buildValidTokenResult(TOKEN_ATTRIBUTES); + } + + @Test + void shouldReturnValidResult() { + var result = validate(); + + assertThat(result).isSameAs(VALIDATION_RESULT); + } + } + + @Nested + class TestInvalid { + + @Mock + private TokenVerificationException exception; + + @DisplayName("should call buildInvalidTokenResult if parseToken throws exception") + @Test + void shouldCallBuildInvalidTokenResult1() { + doThrow(exception).when(service).parseToken(anyString()); + + validate(); + + verify(service).buildInvalidTokenResult(exception); + } + + @DisplayName("should call buildInvalidTokenResult if getAttributes throws exception") + @Test + void shouldCallBuildInvalidTokenResult2() { + doReturn(parsedToken).when(service).parseToken(anyString()); + doThrow(exception).when(service).getAttributes(any()); + + validate(); + + verify(service).buildInvalidTokenResult(exception); + } + + @Test + void shouldReturnInvalidResult() { + doThrow(exception).when(service).parseToken(anyString()); + var invalidTokenResult = TokenValidationResultTestFactory.createInvalid(); + doReturn(invalidTokenResult).when(service).buildInvalidTokenResult(exception); + + var result = validate(); + + assertThat(result).isSameAs(invalidTokenResult); + } + } + + private TokenValidationResult validate() { + return service.validate(SAML_TOKEN); + } + } + + @Nested + class TestParseToken { + + private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7); + + @Mock + private Response response; + @Mock + private InputStream tokenStream; + @Mock + private Document tokenDocument; + @Mock + private Element tokenElement; + + @BeforeEach + void init() { + doReturn(tokenStream).when(service).buildInputStream(anyString()); + } + + @Nested + class TestParsingSuccessfully { + @Captor + private ArgumentCaptor<InputStream> tokenStreamCaptor; + + @SneakyThrows + @BeforeEach + void init() { + when(tokenDocument.getDocumentElement()).thenReturn(tokenElement); + when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument); + } + + @Test + void shouldCallBuildInputStream() { + parseToken(); + + verify(service).buildInputStream(SAML_TOKEN); + } + + @SneakyThrows + @Test + void shouldCallParserPool() { + parseToken(); + + verify(parserPool).parse(tokenStreamCaptor.capture()); + assertThat(tokenStreamCaptor.getValue()).isSameAs(tokenStream); + } + + @SneakyThrows + @Test + void shouldCallUnmarshall() { + parseToken(); + + verify(responseUnmarshaller).unmarshall(tokenElement); + } + + @SneakyThrows + @Test + void shouldReturnResponse() { + doReturn(response).when(responseUnmarshaller).unmarshall(any()); + + var result = parseToken(); + + assertThat(result).isSameAs(response); + } + } + + @Nested + class TestParsingFails { + + @SneakyThrows + @DisplayName("should throw TokenVerificationException on IOException") + @Test + void shouldThrowOnIOException() { + when(tokenDocument.getDocumentElement()).thenReturn(tokenElement); + when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument); + var exception = new IOException(); + doThrow(exception).when(tokenStream).close(); + + Assertions.assertThatThrownBy(TestParseToken.this::parseToken) + .isInstanceOf(TokenVerificationException.class) + .hasCause(exception); + } + + @SneakyThrows + @DisplayName("should throw TokenVerificationException on XMLParserException") + @Test + void shouldThrowOnXMLParserException() { + var exception = new XMLParserException(); + doThrow(exception).when(parserPool).parse(any(InputStream.class)); + + Assertions.assertThatThrownBy(TestParseToken.this::parseToken) + .isInstanceOf(TokenVerificationException.class) + .hasCause(exception); + } + + @SneakyThrows + @DisplayName("should throw TokenVerificationException on UnmarshallingException") + @Test + void shouldThrowOnUnmarshallingException() { + when(tokenDocument.getDocumentElement()).thenReturn(tokenElement); + when(parserPool.parse(any(InputStream.class))).thenReturn(tokenDocument); + var exception = new UnmarshallingException(); + doThrow(exception).when(responseUnmarshaller).unmarshall(any()); + + Assertions.assertThatThrownBy(TestParseToken.this::parseToken) + .isInstanceOf(TokenVerificationException.class) + .hasCause(exception); + } + } + + private Response parseToken() { + return service.parseToken(SAML_TOKEN); + } + } + + @Nested + class TestBuildInputStream { + + private static final String SAML_TOKEN = LoremIpsum.getInstance().getWords(7); + + @SneakyThrows + @Test + void shouldReturnInputStream() { + var result = service.buildInputStream(SAML_TOKEN); + + assertThat(result.readAllBytes()).isEqualTo(SAML_TOKEN.getBytes()); + } + } + + @Nested + class TestGetAttributes { + + private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(3); + private static final TokenAttributes TOKEN_ATTRIBUTES = TokenAttributesTestFactory.create(); + + @Mock + private Response token; + @Mock + private SamlAttributeService samlAttributeService; + + @BeforeEach + void init() { + doReturn(TOKEN_ISSUER).when(service).getTokenIssuer(any()); + doReturn(samlAttributeService).when(service).getSamlAttributeService(any()); + doReturn(TOKEN_ATTRIBUTES).when(samlAttributeService).getAttributes(any()); + } + + @Test + void shouldCallGetTokenIssuer() { + getAttributes(); + + verify(service).getTokenIssuer(token); + } + + @Test + void shouldCallGetSamlAttributeService() { + getAttributes(); + + verify(service).getSamlAttributeService(TOKEN_ISSUER); + } + + @Test + void shouldCallGetAttributes() { + getAttributes(); + + verify(samlAttributeService).getAttributes(token); + } + + @Test + void shouldReturnTokenAttributes() { + var result = getAttributes(); + + assertThat(result).isSameAs(TOKEN_ATTRIBUTES); + } + + private TokenAttributes getAttributes() { + return service.getAttributes(token); + } + } + + @Nested + class TestGetTokenIssuer { + + private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(3); + + @Mock + private Response token; + @Mock + private Issuer issuer; + + @Test + void shouldReturnTokenIssuer() { + when(token.getIssuer()).thenReturn(issuer); + when(issuer.getValue()).thenReturn(TOKEN_ISSUER); + + var result = service.getTokenIssuer(token); + + assertThat(result).isEqualTo(TOKEN_ISSUER); + } + + @Test + void shouldThrowOnNoIssuer() { + when(token.getIssuer()).thenReturn(null); + + assertThatThrownBy(() -> service.getTokenIssuer(token)).isInstanceOf(TokenVerificationException.class); + } + } + + @Nested + class TestGetSamlAttributeService { + + private static final String TOKEN_ISSUER = LoremIpsum.getInstance().getWords(1); + + @Mock + private SamlAttributeService samlAttributeService; + + @Test + void shouldCallGetService() { + when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.of(samlAttributeService)); + + getSamlAttributeService(); + + verify(samlServiceRegistry).getService(TOKEN_ISSUER); + } + + @Test + void shouldReturnService() { + when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.of(samlAttributeService)); + + var result = getSamlAttributeService(); + + assertThat(result).isSameAs(samlAttributeService); + } + + @Test + void shouldThrowOnNoService() { + when(samlServiceRegistry.getService(anyString())).thenReturn(Optional.empty()); + + assertThatThrownBy(this::getSamlAttributeService).isInstanceOf(TechnicalException.class); + } + + private SamlAttributeService getSamlAttributeService() { + return service.getSamlAttributeService(TOKEN_ISSUER); + } + } + + @Nested + class TestBuildValidTokenResult { + + @Test + void shouldBuildResult() { + var result = service.buildValidTokenResult(TokenAttributesTestFactory.create()); + + assertThat(result).usingRecursiveComparison().isEqualTo(TokenValidationResultTestFactory.createValid()); + } + } + + @Nested + class TestBuildInvalidTokenResult { + + @Test + void shouldBuildResult() { + var exception = new TokenVerificationException("msg", List.of(ValidationErrorTestFactory.create())); + + var result = service.buildInvalidTokenResult(exception); + + assertThat(result).usingRecursiveComparison().isEqualTo(TokenValidationResultTestFactory.createInvalid()); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenTestUtils.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenTestUtils.java deleted file mode 100644 index 985b5ced1080bd55b275976d858ea5ac781d30c4..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenTestUtils.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 de.ozgcloud.token.TokenCheckService.*; -import static org.mockito.Mockito.*; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.jetbrains.annotations.NotNull; -import org.springframework.core.io.InputStreamResource; - -import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.token.TokenCheckConfiguration; -import de.ozgcloud.token.TokenCheckProperties; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.xml.BasicParserPool; -import net.shibboleth.utilities.java.support.xml.ParserPool; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class SamlTokenTestUtils { - public static final String BAYERN_ID = "bayernId"; - public static final String MUK = "muk"; - public static final String IDP_ENTITY_ID_BAYERN_ID = "https://infra-pre-id.bayernportal.de/idp"; - public static final String IDP_ENTITY_ID_MUK = "https://e4k-portal.een.elster.de"; - - static ParserPool initParserPool() throws ComponentInitializationException { - var localParserPool = new BasicParserPool(); - - final var features = SamlTokenUtils.createFeatureMap(); - localParserPool.setBuilderFeatures(features); - localParserPool.setBuilderAttributes(new HashMap<>()); - localParserPool.initialize(); - - return localParserPool; - } - - public static SamlConfigurationRegistry initConfig(String type) throws ComponentInitializationException { - TokenCheckProperties properties = null; - if (BAYERN_ID.equals(type)) { - properties = initProperties(); - } else if (MUK.equals(type)) { - properties = initMukProperties(); - } - - var samlConfigurationRegistry = new SamlConfigurationRegistry(); - var config = new TokenCheckConfiguration(properties, initParserPool(), samlConfigurationRegistry); - config.initOpenSAML(); - - return samlConfigurationRegistry; - } - - static @NotNull TokenCheckProperties initProperties() { - TokenCheckProperties properties = mock(TokenCheckProperties.class); - var entity = new ConfigurationEntity(); - entity.setIdpEntityId(IDP_ENTITY_ID_BAYERN_ID); - entity.setCertificate(new InputStreamResource(TestUtils.loadFile("test1-enc.crt"))); - entity.setKey(new InputStreamResource(TestUtils.loadFile("test1-enc.key"))); - entity.setMetadata(new InputStreamResource(TestUtils.loadFile("metadata/bayernid-idp-infra.xml"))); - entity.setMappings(Map.of(POSTKORB_HANDLE_KEY, "urn:oid:2.5.4.18", TRUST_LEVEL_KEY, "urn:oid:1.2.40.0.10.2.1.1.261.94")); - when(properties.getEntities()).thenReturn(List.of(entity)); - return properties; - } - - static @NotNull TokenCheckProperties initMukProperties() { - TokenCheckProperties properties = mock(TokenCheckProperties.class); - var entity = new ConfigurationEntity(); - entity.setIdpEntityId(IDP_ENTITY_ID_MUK); - entity.setCertificate(new InputStreamResource(TestUtils.loadFile("test3-enc.crt"))); - entity.setKey(new InputStreamResource(TestUtils.loadFile("test3-enc.key"))); - entity.setMetadata(new InputStreamResource(TestUtils.loadFile("metadata/muk-idp-e4k.xml"))); - entity.setMappings(Map.of(TRUST_LEVEL_KEY, "Elster")); - entity.setUseIdAsPostkorbHandle(true); - when(properties.getEntities()).thenReturn(List.of(entity)); - return properties; - } -} diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenUtilsTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenUtilsTest.java deleted file mode 100644 index dc79a03ecfeab3c0c6a543c3612d03cba4f019f7..0000000000000000000000000000000000000000 --- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenUtilsTest.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * 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 de.ozgcloud.token.saml.SamlTokenUtils.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.opensaml.core.config.ConfigurationService; -import org.opensaml.core.config.InitializationService; -import org.opensaml.core.xml.XMLObject; -import org.opensaml.core.xml.config.XMLObjectProviderRegistry; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; -import org.opensaml.saml.saml2.metadata.KeyDescriptor; -import org.opensaml.security.credential.UsageType; -import org.opensaml.xmlsec.signature.KeyInfo; -import org.opensaml.xmlsec.signature.X509Data; -import org.springframework.core.io.InputStreamResource; -import org.springframework.core.io.Resource; -import org.springframework.security.saml2.Saml2Exception; - -import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.token.TokenCheckProperties; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.xml.BasicParserPool; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import net.shibboleth.utilities.java.support.xml.XMLParserException; - -@Disabled("need to refactor: The keyfile is empty") -class SamlTokenUtilsTest { - private XMLObjectProviderRegistry registry; - private ParserPool parserPool; - - @BeforeEach - void setup() throws ComponentInitializationException { - parserPool = parserPool(); - initOpenSAML(); - } - - private ParserPool parserPool() throws ComponentInitializationException { - var localParserPool = new BasicParserPool(); - - final var features = SamlTokenUtils.createFeatureMap(); - localParserPool.setBuilderFeatures(features); - localParserPool.setBuilderAttributes(new HashMap<>()); - localParserPool.initialize(); - - return localParserPool; - } - - public void initOpenSAML() { - try { - registry = new XMLObjectProviderRegistry(); - ConfigurationService.register(XMLObjectProviderRegistry.class, registry); - - registry.setParserPool(parserPool); - InitializationService.initialize(); - } catch (Exception e) { - throw new RuntimeException("Initialization failed"); - } - } - - XMLObject xmlObject(InputStream inputStream) throws XMLParserException { - var document = parserPool.parse(inputStream); - var element = document.getDocumentElement(); - var unmarshaller = registry.getUnmarshallerFactory().getUnmarshaller(element); - if (unmarshaller == null) { - throw new Saml2Exception("Unsupported element of type " + element.getTagName()); - } - try { - return unmarshaller.unmarshall(element); - } catch (Exception ex) { - throw new Saml2Exception(ex); - } - } - - @Nested - class TestXmlFeatureMapCreation { - @Test - void shouldCreateFeatureMap() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).isNotNull(); - } - - @Test - void shouldHaveExternalGeneralEntitiesFalse() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).containsEntry(FEATURES_EXTERNAL_GENERAL_ENTITIES, Boolean.FALSE); - } - - @Test - void shouldHaveExternalParameterEntitiesFalse() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).containsEntry(FEATURES_EXTERNAL_PARAMETER_ENTITIES, Boolean.FALSE); - } - - @Test - void shouldHaveDisallowDocTypeDeclTrue() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).containsEntry(FEATURES_DISALLOW_DOCTYPE_DECL, Boolean.TRUE); - } - - @Test - void shouldHaveValidationSchemaNormalizedFalse() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).containsEntry(VALIDATION_SCHEMA_NORMALIZED_VALUE, Boolean.FALSE); - } - - @Test - void shouldHaveSecureProcessingTrue() { - var map = SamlTokenUtils.createFeatureMap(); - - assertThat(map).containsEntry(FEATURE_SECURE_PROCESSING, Boolean.TRUE); - } - } - - @Nested - class TestLoadingDecryptionCredentials { - private Resource key; - private Resource certificate; - - @BeforeEach - void setUp() { - TokenCheckProperties tokenCheckerProperties = SamlTokenTestUtils.initProperties(); - key = tokenCheckerProperties.getEntities().getFirst().getKey(); - certificate = tokenCheckerProperties.getEntities().getFirst().getCertificate(); - } - - @Test - void shouldLoadDecryptionCredentials() { - var credentials = SamlTokenUtils.getDecryptionCredential(key, certificate); - - assertThat(credentials).isNotNull(); - } - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfMissingCertificate() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SamlTokenUtils.getDecryptionCredential(key, null)) - .withMessageStartingWith(NO_CERTIFICATE_LOCATION_SPECIFIED); - } - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfMissingKey() { - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SamlTokenUtils.getDecryptionCredential(null, certificate)) - .withMessageStartingWith(NO_PRIVATE_KEY_LOCATION_SPECIFIED); - } - - @Nested - class TestMissingLocations { - @Mock - private Resource keyMock; - @Mock - private Resource certificateMock; - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfMissingCertificateLocation() { - when(certificateMock.exists()).thenReturn(Boolean.FALSE); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> SamlTokenUtils.getDecryptionCredential(key, certificateMock)) - .withMessageStartingWith(CERTIFICATE_LOCATION); - } - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfCertificateIOException() throws IOException { - when(certificateMock.exists()).thenReturn(Boolean.TRUE); - when(certificateMock.getInputStream()).thenThrow(new IOException("test")); - - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( - () -> SamlTokenUtils.getDecryptionCredential(key, certificateMock)); - } - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfMissingKeyLocation() { - when(keyMock.exists()).thenReturn(Boolean.FALSE); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy( - () -> SamlTokenUtils.getDecryptionCredential(keyMock, certificate)).withMessageStartingWith(PRIVATE_KEY_LOCATION); - } - - @Test - void shouldNotLoadDecryptionCredentialsBecauseOfKeyIOException() throws IOException { - when(keyMock.exists()).thenReturn(Boolean.TRUE); - when(keyMock.getInputStream()).thenThrow(new IOException("test")); - - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( - () -> SamlTokenUtils.getDecryptionCredential(keyMock, certificate)); - } - } - } - - @Nested - class TestLoadingEntityDescriptor { - @Test - void shouldLoadEntityDescriptor() throws IOException, XMLParserException { - var metadata = xmlObject(new InputStreamResource(TestUtils.loadFile("metadata/bayernid-idp-infra.xml")).getInputStream()); - - var entityDescriptor = SamlTokenUtils.findEntityDescriptor(metadata); - - assertThat(entityDescriptor).isNotEmpty(); - } - - @Test - void shouldLoadEntitiesDescriptor() throws IOException, XMLParserException { - var metadata = xmlObject(new InputStreamResource(TestUtils.loadFile("metadata/mujina_metadata.xml")).getInputStream()); - - var entityDescriptor = SamlTokenUtils.findEntityDescriptor(metadata); - - assertThat(entityDescriptor).isNotEmpty(); - } - - @Test - void shouldNotLoadEntityDescriptor() { - var entityDescriptor = SamlTokenUtils.findEntityDescriptor(null); - - assertThat(entityDescriptor).isEmpty(); - } - - @Test - void shouldNotLoadEntityDescriptorMissing() throws IOException, XMLParserException { - var metadata = xmlObject(new InputStreamResource(TestUtils.loadFile("metadata/missing_IDPSSODescriptor.xml")).getInputStream()); - - var entityDescriptor = SamlTokenUtils.findEntityDescriptor(metadata); - - assertThat(entityDescriptor).isEmpty(); - } - } - - @Nested - class TestLoadingVerificationCertificatesFromIDPMetadata { - - @Test - void shouldLoadVerificationCertificates() throws IOException, XMLParserException { - var metadata = xmlObject(new InputStreamResource(TestUtils.loadFile("metadata/bayernid-idp-infra.xml")).getInputStream()); - - var credentials = SamlTokenUtils.getVerificationCertificates(SamlTokenUtils.findEntityDescriptor(metadata).get()); - - assertThat(credentials).isNotEmpty(); - } - - @Test - void shouldNotLoadVerificationCertificatesBecauseOfException() throws IOException, XMLParserException { - var metadata = xmlObject(new InputStreamResource(TestUtils.loadFile("metadata/invalid_IDPSSODescriptor.xml")).getInputStream()); - var cert = SamlTokenUtils.findEntityDescriptor(metadata).get(); - - assertThatExceptionOfType(Saml2Exception.class).isThrownBy( - () -> SamlTokenUtils.getVerificationCertificates(cert)) - .withMessageStartingWith(SAML_ASSERTIONS_VERIFICATION_EMPTY); - } - - @Test - void shouldNotLoadVerificationCertificatesBecauseMissing() { - var entityDescriptor = mock(EntityDescriptor.class); - when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(null); - - assertThatExceptionOfType(Saml2Exception.class).isThrownBy( - () -> SamlTokenUtils.getVerificationCertificates(entityDescriptor)) - .withMessageStartingWith(MISSING_THE_NECESSARY_IDPSSODESCRIPTOR_ELEMENT); - } - - @Test - void shouldNotLoadCertificates() { - var entityDescriptor = initTestData(); - - assertThatExceptionOfType(Saml2Exception.class).isThrownBy( - () -> SamlTokenUtils.getVerificationCertificates(entityDescriptor)); - } - - private static @NotNull EntityDescriptor initTestData() { - var entityDescriptor = mock(EntityDescriptor.class); - var iDPSSODescriptor = mock(IDPSSODescriptor.class); - var keyDescriptor = mock(KeyDescriptor.class); - var keyInfo = mock(KeyInfo.class); - var x509Data = mock(X509Data.class); - var x509Certificate = mock(org.opensaml.xmlsec.signature.X509Certificate.class); - when(x509Certificate.getValue()).thenReturn( - "-----BEGIN CERTIFICATE-----\nMIIEGzCCAwOgAwIBAgIUWPZFfhB4+iI3XdjUTMqhhDkljGgwDQYJKoZIhvcNAQEL\n-----END CERTIFICATE-----"); - when(x509Data.getX509Certificates()).thenReturn(List.of(x509Certificate)); - when(keyInfo.getX509Datas()).thenReturn(List.of(x509Data)); - when(keyDescriptor.getKeyInfo()).thenReturn(keyInfo); - when(keyDescriptor.getUse()).thenReturn(UsageType.SIGNING); - when(iDPSSODescriptor.getKeyDescriptors()).thenReturn(List.of(keyDescriptor)); - when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(iDPSSODescriptor); - return entityDescriptor; - } - } -} \ 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..7d40d60227a60a08535b84b5f1b81d85a0470984 --- /dev/null +++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java @@ -0,0 +1,688 @@ +/* + * 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.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.InputStream; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistry; +import org.opensaml.core.xml.io.Unmarshaller; +import org.opensaml.core.xml.io.UnmarshallerFactory; +import org.opensaml.core.xml.io.UnmarshallingException; +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.IDPSSODescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.security.credential.UsageType; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.security.x509.BasicX509Credential; +import org.opensaml.security.x509.X509Credential; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.opensaml.xmlsec.signature.KeyInfo; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; +import org.springframework.core.io.Resource; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty; +import lombok.SneakyThrows; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; + +class SamlTrustEngineFactoryTest { + + private static final String IDP_ENTITY_ID = UUID.randomUUID().toString(); + + @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; + + @Nested + class TestBuildSuccessfully { + + @Mock + private X509Credential x509Credential; + + @BeforeEach + void init() { + when(tokenValidationProperty.getMetadata()).thenReturn(metadata); + when(tokenValidationProperty.getIdpEntityId()).thenReturn(IDP_ENTITY_ID); + doReturn(Stream.of(credential)).when(factory).getCertificatesFromMetadata(any()); + doReturn(x509Credential).when(factory).buildBasicX509Credential(any(), any()); + } + + @Test + void shouldCallGetCertificatesFromMetadata() { + buildCredentialResolver(); + + verify(factory).getCertificatesFromMetadata(metadata); + } + + @Test + void shouldCallBuildBasicX509Credential() { + buildCredentialResolver(); + + verify(factory).buildBasicX509Credential(credential, IDP_ENTITY_ID); + } + + @Test + void shouldBuildCredentialResolver() { + var result = buildCredentialResolver(); + + assertThat(result.getCollection()).containsExactly(x509Credential); + } + } + + @Nested + class TestThrowException { + + @Test + void shouldThrowExceptionWhenNoCertificates() { + doReturn(Stream.empty()).when(factory).getCertificatesFromMetadata(any()); + + assertThrows(TechnicalException.class, TestBuildCredentialResolver.this::buildCredentialResolver); + } + } + + private CollectionCredentialResolver buildCredentialResolver() { + return factory.buildCredentialResolver(tokenValidationProperty); + } + } + + @Nested + class TestGetCertificatesFromMetadata { + + @Mock + private Resource metadata; + @Mock + private InputStream inputStream; + @Mock + private XMLObject xmlObject; + @Mock + private Saml2X509Credential credential; + @Mock + private EntityDescriptor entityDescriptor; + + @BeforeEach + @SneakyThrows + void init() { + when(metadata.getInputStream()).thenReturn(inputStream); + doReturn(xmlObject).when(factory).getXmlObject(any()); + } + + @Nested + class TestSuccessfully { + + @BeforeEach + void init() { + doReturn(Optional.of(entityDescriptor)).when(factory).findEntityDescriptor(any()); + doReturn(Stream.of(credential)).when(factory).getVerificationCertificates(any()); + } + + @Test + void shouldCallGetXmlObject() { + getCertificatesFromMetadata(); + + verify(factory).getXmlObject(inputStream); + } + + @Test + void shouldCallFindEntityDescriptor() { + getCertificatesFromMetadata(); + + verify(factory).findEntityDescriptor(xmlObject); + } + + @Test + void shouldGetVerificationCertificates() { + getCertificatesFromMetadata(); + + verify(factory).getVerificationCertificates(entityDescriptor); + } + + @Test + void shouldReturnResult() { + var result = getCertificatesFromMetadata(); + + assertThat(result).containsExactly(credential); + } + } + + @Nested + class TestWithException { + + @Test + void shouldThrowException() { + doReturn(Optional.empty()).when(factory).findEntityDescriptor(any()); + + assertThrows(TechnicalException.class, TestGetCertificatesFromMetadata.this::getCertificatesFromMetadata); + } + } + + private List<Saml2X509Credential> getCertificatesFromMetadata() { + return factory.getCertificatesFromMetadata(metadata).toList(); + } + + } + + @Nested + class TestGetXmlObject { + + @Mock + private InputStream inputStream; + @Mock + private Unmarshaller unmarshaller; + @Mock + private XMLObject xmlObject; + @Mock + private Document document; + @Mock + private Element element; + + @Nested + class TestSuccessfully { + @BeforeEach + @SneakyThrows + void init() { + when(parserPool.parse(any(InputStream.class))).thenReturn(document); + when(document.getDocumentElement()).thenReturn(element); + doReturn(unmarshaller).when(factory).getUnmarshaller(any()); + when(unmarshaller.unmarshall(element)).thenReturn(xmlObject); + } + + @Test + @SneakyThrows + void shouldCallParse() { + getXmlObject(); + + verify(parserPool).parse(inputStream); + } + + @Test + void shouldCallGetUnmarshaller() { + getXmlObject(); + + verify(factory).getUnmarshaller(element); + } + + @Test + @SneakyThrows + void shouldCallUnmarshall() { + getXmlObject(); + + verify(unmarshaller).unmarshall(element); + } + + @Test + void shouldReturnResult() { + var result = getXmlObject(); + + assertThat(result).isEqualTo(xmlObject); + } + } + + @Nested + class TestWithException { + + @SneakyThrows + @Test + void shouldThrowExceptionIfXMLParseException() { + when(parserPool.parse(any(InputStream.class))).thenThrow(XMLParserException.class); + + assertThrows(TechnicalException.class, TestGetXmlObject.this::getXmlObject); + } + + @SneakyThrows + @Test + void shouldThrowExceptionIfUnmarshallingException() { + when(parserPool.parse(any(InputStream.class))).thenReturn(document); + when(document.getDocumentElement()).thenReturn(element); + doReturn(unmarshaller).when(factory).getUnmarshaller(any()); + when(unmarshaller.unmarshall(element)).thenThrow(UnmarshallingException.class); + + assertThrows(TechnicalException.class, TestGetXmlObject.this::getXmlObject); + } + } + + private XMLObject getXmlObject() { + return factory.getXmlObject(inputStream); + } + } + + @Nested + class TestGetUnmarshaller { + + @Mock + private Element element; + @Mock + private UnmarshallerFactory unmarshallerFactory; + @Mock + private Unmarshaller unmarshaller; + + @BeforeEach + void init() { + when(registry.getUnmarshallerFactory()).thenReturn(unmarshallerFactory); + } + + @Nested + class TestSuccessfully { + + @BeforeEach + void init() { + when(unmarshallerFactory.getUnmarshaller(any(Element.class))).thenReturn(unmarshaller); + } + + @Test + void shouldCallGetUnmarshallerFactory() { + getUnmarshaller(); + + verify(registry).getUnmarshallerFactory(); + } + + @Test + void shouldCallGetUnmarshaller() { + getUnmarshaller(); + + verify(unmarshallerFactory).getUnmarshaller(element); + } + + @Test + void shouldReturnResult() { + var result = getUnmarshaller(); + + assertThat(result).isEqualTo(unmarshaller); + } + } + + @Nested + class TestNoUnmarshaller { + + @Test + void shouldThrowException() { + assertThrows(TechnicalException.class, TestGetUnmarshaller.this::getUnmarshaller); + } + } + + private Unmarshaller getUnmarshaller() { + return factory.getUnmarshaller(element); + } + } + + @Nested + class TestFindEntityDescriptor { + + @Mock + private XMLObject xmlObject; + @Mock + private EntityDescriptor entityDescriptor; + @Mock + private IDPSSODescriptor descriptor; + + @BeforeEach + void init() { + when(factory.extractEntityDescriptor(any())).thenReturn(Optional.of(entityDescriptor)); + } + + @Test + void shouldCallExtractEntityDescriptor() { + findEntityDescriptor(); + + verify(factory).extractEntityDescriptor(xmlObject); + } + + @Test + void shouldFilterDescriptor() { + when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(descriptor); + + findEntityDescriptor(); + + verify(entityDescriptor).getIDPSSODescriptor(SAMLConstants.SAML20P_NS); + } + + @Test + void shouldReturnResult() { + when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(descriptor); + + var result = findEntityDescriptor(); + + assertThat(result).contains(entityDescriptor); + } + + private Optional<EntityDescriptor> findEntityDescriptor() { + return factory.findEntityDescriptor(xmlObject); + } + } + + @Nested + class TestExtractEntityDescriptor { + + @Mock + private XMLObject xmlObject; + + @Test + void shouldReturnEmpty() { + var result = factory.extractEntityDescriptor(xmlObject); + + assertThat(result).isEmpty(); + } + + @Nested + class TestEntityDescriptor { + + @Mock + private EntityDescriptor entityDescriptor; + + @Test + void shouldReturnEntityDescriptor() { + var result = factory.extractEntityDescriptor(entityDescriptor); + + assertThat(result).contains(entityDescriptor); + } + } + + @Nested + class TestEntitiesDescriptor { + + @Mock + private EntitiesDescriptor entitiesDescriptor; + @Mock + private EntityDescriptor entityDescriptor; + + @BeforeEach + void init() { + when(entitiesDescriptor.getEntityDescriptors()).thenReturn(List.of(entityDescriptor)); + } + + @Test + void shouldReturnEntityDescriptor() { + var result = factory.extractEntityDescriptor(entitiesDescriptor); + + assertThat(result).contains(entityDescriptor); + } + } + } + + @Nested + class TestGetVerificationCertificates { + + @Mock + private EntityDescriptor descriptor; + + @Nested + class TestSuccessfully { + + @Mock + private IDPSSODescriptor idpSsoDescriptor; + @Mock + private KeyDescriptor keyDescriptor; + @Mock + private X509Certificate certificate; + + @BeforeEach + void init() { + when(descriptor.getIDPSSODescriptor(anyString())).thenReturn(idpSsoDescriptor); + when(idpSsoDescriptor.getKeyDescriptors()).thenReturn(List.of(keyDescriptor)); + doReturn(true).when(factory).isSignatureKey(any()); + doReturn(Stream.of(certificate)).when(factory).getCertificates(any()); + } + + @Test + void shouldCallGetKeyDescriptors() { + getVerificationCertificates(); + + verify(idpSsoDescriptor).getKeyDescriptors(); + } + + @Test + void shouldCallIsSignatureKey() { + getVerificationCertificates(); + + verify(factory).isSignatureKey(keyDescriptor); + } + + @Test + void shouldCallGetCertificates() { + getVerificationCertificates(); + + verify(factory).getCertificates(keyDescriptor); + } + + @Test + void shouldCallVerification() { + try (var credentialMock = mockStatic(Saml2X509Credential.class)) { + getVerificationCertificates(); + + credentialMock.verify(() -> Saml2X509Credential.verification(certificate)); + } + } + } + + @Nested + class TestWithException { + + @Test + void shouldThrowException() { + assertThrows(TechnicalException.class, TestGetVerificationCertificates.this::getVerificationCertificates); + } + } + + private List<Saml2X509Credential> getVerificationCertificates() { + return factory.getVerificationCertificates(descriptor).toList(); + } + } + + @Nested + class TestIsSignatureKey { + + @Mock + private KeyDescriptor keyDescriptor; + + @Test + void shouldReturnTrue() { + when(keyDescriptor.getUse()).thenReturn(UsageType.SIGNING); + + var result = isSignatureKey(); + + assertThat(result).isTrue(); + } + + @DisplayName("should return false") + @ParameterizedTest(name = "when keyDescriptor use is {0}") + @EnumSource(value = UsageType.class, names = { "SIGNING" }, mode = EnumSource.Mode.EXCLUDE) + void shouldReturnFalse(UsageType use) { + when(keyDescriptor.getUse()).thenReturn(use); + + var result = isSignatureKey(); + + assertThat(result).isFalse(); + } + + private boolean isSignatureKey() { + return factory.isSignatureKey(keyDescriptor); + } + } + + @Nested + class TestGetCertificates { + + @Mock + private KeyDescriptor keyDescriptor; + @Mock + private KeyInfo keyInfo; + @Mock + private X509Certificate certificate; + + @BeforeEach + void init() { + when(keyDescriptor.getKeyInfo()).thenReturn(keyInfo); + } + + @Test + void shouldCallGetCertificates() { + try (var keyInfoSupportMock = mockStatic(KeyInfoSupport.class)) { + getCertificates(); + + keyInfoSupportMock.verify(() -> KeyInfoSupport.getCertificates(keyInfo)); + } + } + + @Test + void shouldReturnResult() { + try (var keyInfoSupportMock = mockStatic(KeyInfoSupport.class)) { + keyInfoSupportMock.when(() -> KeyInfoSupport.getCertificates(any(KeyInfo.class))).thenReturn(List.of(certificate)); + + var result = getCertificates(); + + assertThat(result).containsExactly(certificate); + } + } + + @Test + void shouldThrowException() { + try (var keyInfoSupportMock = mockStatic(KeyInfoSupport.class)) { + keyInfoSupportMock.when(() -> KeyInfoSupport.getCertificates(any(KeyInfo.class))).thenThrow(CertificateException.class); + + assertThrows(TechnicalException.class, this::getCertificates); + + keyInfoSupportMock.verify(() -> KeyInfoSupport.getCertificates(keyInfo)); + } + } + + private List<X509Certificate> getCertificates() { + return factory.getCertificates(keyDescriptor).toList(); + } + } + + @Nested + class TestBuildBasicX509Credential { + + @Mock + private Saml2X509Credential samlKey; + @Mock + private X509Certificate certificate; + + @BeforeEach + void init() { + when(samlKey.getCertificate()).thenReturn(certificate); + } + + @Test + void shouldSetEntityCertificate() { + var result = buildBasicX509Credential(); + + assertThat(result.getEntityCertificate()).isEqualTo(certificate); + } + + @Test + void shouldSetUsageType() { + var result = buildBasicX509Credential(); + + assertThat(result.getUsageType()).isEqualTo(UsageType.SIGNING); + } + + @Test + void shouldSetEntityId() { + var result = buildBasicX509Credential(); + + assertThat(result.getEntityId()).isEqualTo(IDP_ENTITY_ID); + } + + private BasicX509Credential buildBasicX509Credential() { + return (BasicX509Credential) factory.buildBasicX509Credential(samlKey, IDP_ENTITY_ID); + } + } +} \ No newline at end of file diff --git a/token-checker-server/src/test/resources/application-itcase.yml b/token-checker-server/src/test/resources/application-itcase.yml new file mode 100644 index 0000000000000000000000000000000000000000..1c0f3971dfa214cd541b3bf13c9d2ba5f5b2cc17 --- /dev/null +++ b/token-checker-server/src/test/resources/application-itcase.yml @@ -0,0 +1,20 @@ +logging: + level: + ROOT: ERROR, + "org.springframework": ERROR + "org.opensaml.xmlsec.encryption.support": DEBUG + "de.ozgcloud": INFO + config: classpath:log4j2-local.xml +ozgcloud: + token: + check: + entities: + - idpEntityId: "https://infra-pre-id.bayernportal.de/idp" + key: "classpath:test1-enc.key" + certificate: "classpath:test1-enc.crt" + metadata: "classpath:metadata/bayernid-idp-infra.xml" + mappings: + postfachId: "urn:oid:2.5.4.18" + trustLevel: "urn:oid:1.2.40.0.10.2.1.1.261.94" +server: + port: -1 \ No newline at end of file