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