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