Skip to content
Snippets Groups Projects
Commit f8944493 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-4383_abrufen_rueckfragen' (#324) from...

Merge pull request 'OZG-4383_abrufen_rueckfragen' (#324) from OZG-4383_abrufen_rueckfragen into master

Reviewed-on: https://git.ozg-sh.de/ozgcloud-app/vorgang-manager/pulls/324


Reviewed-by: default avatarOZGCloud <ozgcloud@mgm-tp.com>
parents 03fe8422 514a900a
No related branches found
No related tags found
No related merge requests found
Showing
with 1657 additions and 260 deletions
......@@ -24,7 +24,8 @@
unter der Lizenz sind dem Lizenztext zu entnehmen.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
......@@ -40,7 +41,7 @@
<name>OZG-Cloud Nachrichten Manager</name>
<properties>
<java.version>21</java.version>
<java.version>17</java.version>
<!-- TODO version management -->
<shedlock.version>4.25.0</shedlock.version>
<logcaptor.version>2.7.10</logcaptor.version>
......@@ -50,6 +51,33 @@
<bayernid-proxy-interface.version>0.1.0</bayernid-proxy-interface.version>
</properties>
<repositories>
<repository>
<id>shibboleth-releases</id>
<name>Shibboleth Releases Repository</name>
<url>https://build.shibboleth.net/maven/releases/</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>warn</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>shibboleth-thirdparty</id>
<name>Shibboleth Thirdparty Repository</name>
<url>https://build.shibboleth.net/maven/thirdparty/</url>
<releases>
<enabled>true</enabled>
<checksumPolicy>fail</checksumPolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>de.ozgcloud.vorgang</groupId>
......@@ -130,7 +158,12 @@
<artifactId>mapstruct</artifactId>
</dependency>
<!-- grpc -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
......
/*
* 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.nachrichten.antragraum;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import de.ozgcloud.vorgang.grpc.command.GrpcCommand;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
@RequiredArgsConstructor
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
class AntragraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplBase {
private final AntragraumService antragraumService;
private final AntragraumNachrichtMapper mapper;
@Override
public void findRueckfragen(GrpcFindRueckfragenRequest request, StreamObserver<GrpcFindRueckfragenResponse> streamObserver) {
var rueckfragen = antragraumService.findRueckfragen(request.getSamlToken()).map(mapper::toGrpc).toList();
var response = GrpcFindRueckfragenResponse.newBuilder().addAllRueckfrage(rueckfragen).build();
streamObserver.onNext(response);
streamObserver.onCompleted();
}
@Override
public void sendRueckfrageAnswer(GrpcSendRueckfrageAnswerRequest request, StreamObserver<GrpcCommand> streamObserver) {
throw new NotImplementedException("sendRueckfrageAnswer not implemented yet");
}
}
/*
* 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.nachrichten.antragraum;
import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.NullValueCheckStrategy;
import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
interface AntragraumNachrichtMapper {
String DEFAULT_STATUS = "NEU";
@Mapping(source = "sentAt", target = "sentAt", dateFormat = "yyyy-MM-dd'T'HH:mm:ss")
@Mapping(source = "mailBody", target = "text")
@Mapping(source = "subject", target = "vorgangName")
@Mapping(source = "attachments", target = "attachmentFileIdList")
@Mapping(target = "status", constant = DEFAULT_STATUS)
GrpcRueckfrage toGrpc(PostfachNachricht postfachNachricht);
}
/*
* 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.nachrichten.antragraum;
import jakarta.validation.constraints.NotEmpty;
......@@ -5,6 +28,7 @@ import jakarta.validation.constraints.NotEmpty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.validation.annotation.Validated;
import lombok.Getter;
......@@ -23,4 +47,22 @@ public class AntragraumProperties {
@NotEmpty
private String url;
/**
* The entityId as defined the BayernId SAML Metadata
*/
@NotEmpty
private String entityId;
/**
* The uri where to load the idp Metadata from
*/
private Resource metadataUri;
/**
* The location of the private key for decrypting the saml token data
*/
private Resource decryptionPrivateKey;
/**
* The location of the certificate for decrypting the saml token data
*/
private Resource decryptionCertificate;
}
/*
* 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.nachrichten.antragraum;
import static java.util.Objects.*;
import jakarta.annotation.PostConstruct;
import java.util.stream.Stream;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import de.ozgcloud.nachrichten.NachrichtenManagerProperties;
import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService;
import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
public class AntragraumService {
private final PersistPostfachNachrichtService postfachNachrichtService;
private final Saml2Verifier verifier;
private final Saml2Parser parser;
private final Saml2Decrypter decrypter;
static final String USER_NOTIFICATION_TEMPLATE = """
Guten Tag,
......@@ -44,4 +75,18 @@ public class AntragraumService {
return USER_NOTIFICATION_TEMPLATE.formatted(getAntragsraumUrl());
}
public Stream<PostfachNachricht> findRueckfragen(String samlToken) {
verifyToken(samlToken);
var postfachId = decrypter.decryptPostfachId(parser.parse(samlToken));
return postfachNachrichtService.findRueckfragen(postfachId);
}
void verifyToken(String token) {
var errors = verifier.verify(token);
if (CollectionUtils.isNotEmpty(errors)) {
throw new SecurityException("SAML Token verification failed. Errors: %s".formatted(errors));
}
}
}
/*
* 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.nachrichten.antragraum;
import java.io.IOException;
import java.io.InputStream;
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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.ProtocolCriterion;
import org.opensaml.saml.metadata.criteria.role.impl.EvaluableProtocolRoleDescriptorCriterion;
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.CredentialResolver;
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.keyinfo.KeyInfoSupport;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
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.Getter;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
import net.shibboleth.utilities.java.support.xml.ParserPool;
import net.shibboleth.utilities.java.support.xml.XMLParserException;
@Configuration
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
class BayernIdSamlConfiguration {
private XMLObjectProviderRegistry registry;
@Getter
private ParserPool parserPool;
@Autowired
private AntragraumProperties antragraumProperties;
@PostConstruct
void initOpenSAML() {
try {
registry = new XMLObjectProviderRegistry();
ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
parserPool = initParserPool();
registry.setParserPool(parserPool);
InitializationService.initialize();
} catch (Exception e) {
throw new RuntimeException("Initialization failed");
}
}
private ParserPool initParserPool() throws ComponentInitializationException {
var localParserPool = new BasicParserPool();
final var features = createFeatureMap();
localParserPool.setBuilderFeatures(features);
localParserPool.setBuilderAttributes(new HashMap<>());
localParserPool.initialize();
return localParserPool;
}
private Map<String, Boolean> createFeatureMap() {
final Map<String, Boolean> features = new HashMap<>();
features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE);
features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE);
features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE);
features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE);
features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE);
return features;
}
SignatureTrustEngine getTrustEngine() {
Set<Credential> credentials = new HashSet<>();
Collection<Saml2X509Credential> keys = getCertificatesFromMetadata();
for (Saml2X509Credential key : keys) {
var cred = new BasicX509Credential(key.getCertificate());
cred.setUsageType(UsageType.SIGNING);
cred.setEntityId(antragraumProperties.getEntityId());
credentials.add(cred);
}
CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials);
return new ExplicitKeySignatureTrustEngine(credentialsResolver,
DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver());
}
CriteriaSet getVerificationCriteria() {
var criteria = new CriteriaSet();
criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(antragraumProperties.getEntityId())));
criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion("urn:oasis:names:tc:SAML:2.0:protocol")));
criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING)));
return criteria;
}
Saml2X509Credential getDecryptionCredential() {
var privateKey = readPrivateKey(antragraumProperties.getDecryptionPrivateKey());
var certificate = readCertificateFromResource(antragraumProperties.getDecryptionCertificate());
return new Saml2X509Credential(privateKey, certificate, Saml2X509Credential.Saml2X509CredentialType.DECRYPTION);
}
private RSAPrivateKey readPrivateKey(Resource location) {
Assert.state(location != null, "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 X509Certificate readCertificateFromResource(Resource location) {
Assert.state(location != null, "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);
}
}
List<Saml2X509Credential> getCertificatesFromMetadata() {
try (var metadata = antragraumProperties.getMetadataUri().getInputStream()) {
var xmlObject = xmlObject(metadata);
if (xmlObject instanceof EntitiesDescriptor descriptors) {
for (EntityDescriptor descriptor : descriptors.getEntityDescriptors()) {
if (descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) != null) {
return getVerificationCertificates(descriptor);
}
}
}
} catch (IOException e) {
throw new Saml2Exception("Error reading idp metadata.", e);
} catch (ComponentInitializationException | XMLParserException e) {
throw new Saml2Exception("Error initializing parser pool.", e);
}
throw new Saml2Exception("No IDPSSO Descriptors found");
}
private XMLObject xmlObject(InputStream inputStream) throws ComponentInitializationException, XMLParserException {
var document = getParserPool().parse(inputStream);
var element = document.getDocumentElement();
var unmarshaller = this.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);
}
}
List<Saml2X509Credential> getVerificationCertificates(EntityDescriptor descriptor) {
var idpssoDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
if (idpssoDescriptor == null) {
throw new Saml2Exception("Metadata response is 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(
"Metadata response is missing verification certificates, necessary for verifying SAML assertions");
}
return verification;
}
private List<X509Certificate> certificates(KeyDescriptor keyDescriptor) {
try {
return KeyInfoSupport.getCertificates(keyDescriptor.getKeyInfo());
} catch (CertificateException ex) {
throw new Saml2Exception(ex);
}
}
}
/*
* 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.nachrichten.antragraum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import jakarta.annotation.PostConstruct;
import org.opensaml.core.xml.schema.XSString;
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.saml2.encryption.EncryptedElementTypeEncryptedKeyResolver;
import org.opensaml.security.credential.Credential;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
class Saml2Decrypter {
private final BayernIdSamlConfiguration configuration;
public static final String LEGACY_POSTKORB_HANDLE_KEY = "legacyPostkorbHandle";
private Decrypter decrypter;
private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver(
Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(),
new SimpleRetrievalMethodEncryptedKeyResolver()));
@PostConstruct
void init() {
Collection<Credential> credentials = new ArrayList<>();
var decryptionX509Credential = configuration.getDecryptionCredential();
Credential cred = CredentialSupport.getSimpleCredential(decryptionX509Credential.getCertificate(), decryptionX509Credential.getPrivateKey());
credentials.add(cred);
KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials);
var setupDecrypter = new Decrypter(null, resolver, encryptedKeyResolver);
setupDecrypter.setRootInNewDocument(true);
decrypter = setupDecrypter;
}
void decryptResponseElements(Response response) {
for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) {
try {
var assertion = decrypter.decrypt(encryptedAssertion);
response.getAssertions().add(assertion);
} catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
}
String decryptPostfachId(Response response) {
decryptResponseElements(response);
var samlAssertion = response.getAssertions().get(0);
var statements = (AttributeStatement) samlAssertion.getStatements().get(1);
var attributes = statements.getAttributes();
var postfachIdOptional = attributes.stream().filter(attribute -> LEGACY_POSTKORB_HANDLE_KEY.equals(attribute.getFriendlyName())).findFirst();
return postfachIdOptional.map(postfachIdAttribute -> {
var values = postfachIdAttribute.getAttributeValues();
return ((XSString) values.get(0)).getValue();
}).orElseThrow();
}
}
/*
* 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.nachrichten.antragraum;
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.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.saml2.Saml2Exception;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import net.shibboleth.utilities.java.support.xml.XMLParserException;
@Service
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
@RequiredArgsConstructor
class Saml2Parser {
private final BayernIdSamlConfiguration configuration;
private ResponseUnmarshaller unmarshaller;
Response parse(String request) {
return (Response) xmlObject(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8)));
}
XMLObject xmlObject(InputStream inputStream) throws Saml2Exception {
try {
var document = configuration.getParserPool().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;
}
}
/*
* 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.nachrichten.antragraum;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import jakarta.annotation.PostConstruct;
import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@Log4j2
@RequiredArgsConstructor
@Service
@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL)
class Saml2Verifier {
public static final String INVALID_SIGNATURE = "Invalid signature for object [%s]: ";
public static final String SIGNATURE_MISSING = "Signature missing";
private final Saml2Parser parser;
private final BayernIdSamlConfiguration configuration;
private SignatureTrustEngine trustEngine;
private CriteriaSet verificationCriteria;
@PostConstruct
void init() {
trustEngine = configuration.getTrustEngine();
verificationCriteria = configuration.getVerificationCriteria();
}
List<Saml2Error> verify(String samlToken) {
var response = parser.parse(samlToken);
List<Saml2Error> errors = new ArrayList<>();
var signature = response.getSignature();
var profileValidator = new SAMLSignatureProfileValidator();
if (Objects.nonNull(signature)) {
try {
profileValidator.validate(signature);
} catch (Exception ex) {
LOG.error("Error validating SAML Token: ", ex);
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE.formatted(response.getID())));
}
try {
if (!trustEngine.validate(signature, verificationCriteria)) {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE.formatted(response.getID())));
}
} catch (Exception ex) {
LOG.error("Error validating SAML Token: ", ex);
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, INVALID_SIGNATURE.formatted(response.getID())));
}
} else {
errors.add(new Saml2Error(Saml2ErrorCodes.INVALID_SIGNATURE, SIGNATURE_MISSING));
}
return errors;
}
}
ozgcloud:
antragraum:
entityId: https://antragsraum.ozgcloud.de/
\ No newline at end of file
ozgcloud:
antragraum:
url: https://dev.antragsraum.de/
entityId: https://antragsraum.ozgcloud.de/
metadataUri: "classpath:/bayernid/metadata/bayernid-idp-infra.xml"
decryptionPrivateKey: "classpath:/bayernid/bayernid-test-enc.key"
decryptionCertificate: "classpath:/bayernid/bayernid-test-enc.crt"
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?><md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata">
<md:EntityDescriptor entityID="https://infra-pre-id.bayernportal.de/idp">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIFbzCCA1egAwIBAgIJAPdFXXarkBN2MA0GCSqGSIb3DQEBCwUAME4xCzAJBgNV
BAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMQ0wCwYD
VQQKDARBS0RCMQwwCgYDVQQLDANJRE0wHhcNMjAxMDI3MTMxODQxWhcNMjUxMDI2
MTMxODQxWjBOMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQH
DAhNdWVuY2hlbjENMAsGA1UECgwEQUtEQjEMMAoGA1UECwwDSURNMIICIjANBgkq
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzDtWAEdC3J9FD+ti1exRhN1lzNgKWqO2
gQNdJvlt7KGHA2VGGO7tqRogTuoqi/ydtiHJ8+lhp4kcWqyfv7i9HXOncvcsRRmR
dZjUY2Iui6ozJqD5LVm/vP5YfdP7vQPdbqyyfpoJhf3mbMEtdNDdGRnGIPUfDn+C
Fbo37f9tPwMgf3jgh4gxaujtLIhhr9gevVTEeZAFu9EvzLNd3kEtRb7MuXqIOdu1
rW8HlGYFwwVLqEyBn8XG0QAIfhMmGjFMG7z+Kco2quwOmmZVzWQfeH/3AlN2KbcP
t7j+pl+6Bew2AAivP7O+95YKORqQjTu3rPWMF4txPId37MSjoytwBRyd5EACTvhQ
BOGrDFKQUOx6fTtRc8+7XGVz8MdQaZQWQXXh1ByU783twNdnRSrSVIyLdjiy1uCb
jvsSAtbzGBygPIvDo3skCNLNFXsChtHIfFFDK20KPGb0ghEDf2q3hDbFG3ZDGGyn
ZmJcZKuZhJqodJ/++sAXADyTJNAPVYDjKCF4ypELp2Eu/p1gaQPJEb74L/ZFZVOE
JFyXIiaqB9J+fcn/biqHHOmcCi8n9aIiNt1fatr1Z4lQRWoGtKaGU0+bzUSH4Bgs
2EG4u1CI2MKDWqK2aEsHrtu8tbS9LrUmDVKtaEUOeul8xWVa036vp/YUIdiJNZSx
ZG4iTmSOATECAwEAAaNQME4wHQYDVR0OBBYEFFYeltslkaolOmcINXQeSe7nURwp
MB8GA1UdIwQYMBaAFFYeltslkaolOmcINXQeSe7nURwpMAwGA1UdEwQFMAMBAf8w
DQYJKoZIhvcNAQELBQADggIBAKqAlXoO41SAiycYUOrR90pfwTCysmbtHF5RWSCM
jF2aCG8URJ7bXwC0lBH8E5zCetFZwdqZziQtxzRkIOfhS5uWbH0RDhwuxZG+5RTP
yaHPAZI6e5xHDu8vHl/VbC3lnL/6K8l+Purr/yo8qkJqrPgThZRL9jBQyYRhDSsJ
UyIw5zcKKUQC/JWtMQAQcopbjekCs6xDT1HqIN90Sc/gOfYjNo0dGMNmro9mxcw8
2Iow18KNVdtEexfD+/6x4NPD61pzuQEe09TR+Cv3XyzBoGQ/2arijcPnGvth79ff
VFtRSf3fSs7wEKV9g3mEWXFDtPBhDj6K0kKU/kJfEZixkXl92MY+bmugrtTIrazj
tfrgMglIAHu9XCYWd/gef0J+PNfHsxgbTEr3XSC+5/xoFKPQSw3PgV8lkUDq4mJU
Ky/q4YmA37XQxourFR5pWvF03YACdtq6zPjtVeI7Cvkte6k0YW5S3cx9RmPv6YZh
laZ5ERpWNiv6IjokLsvNeemf2PApjO7Q2EDBIoHBYH31wwJSsyRDrSVmbaqLFI15
fLXeh2A4YbaBDZdGvDiLOAk+dG1wdZ2aGw/uNBzMtc8VeKqI1HPcqIluBA3uUPpy
LLA+9hDPf6Pp4j0gkXxBikz+/h22bFxE1HmDiOSkEn+2NmOHuEFeA+D8jsCAL5VJ
3emK</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://infra-pre-id.bayernportal.de/idp/profile/SAML2/POST/SSO"/>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://infra-pre-id.bayernportal.de/idp/profile/SAML2/Redirect/SSO"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>
</md:EntitiesDescriptor>
\ No newline at end of file
/*
* 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.nachrichten.antragraum;
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 de.ozgcloud.nachrichten.postfach.PostfachNachricht;
import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory;
import io.grpc.stub.StreamObserver;
class AntragraumGrpcServiceTest {
@Spy
@InjectMocks
private AntragraumGrpcService antragsraumGrpcService;
@Mock
private AntragraumService antragraumService;
@Mock
private AntragraumNachrichtMapper mapper;
@Nested
class TestFindRueckfragen {
@Nested
class TestFindRueckfragenGrpc {
@Mock
private StreamObserver<GrpcFindRueckfragenResponse> streamObserver;
@BeforeEach
void setup() {
when(antragraumService.findRueckfragen(any())).thenReturn(Stream.of(PostfachNachrichtTestFactory.create()));
when(mapper.toGrpc(any(PostfachNachricht.class))).thenReturn(GrpcRueckfrage.getDefaultInstance());
}
@Test
void shouldCallMapper() {
antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver);
verify(mapper).toGrpc(any(PostfachNachricht.class));
}
@Test
void shouldCallOnNext() {
antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver);
verify(streamObserver).onNext(any(GrpcFindRueckfragenResponse.class));
}
@Test
void shouldCallOnCompleted() {
antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver);
verify(streamObserver).onCompleted();
}
}
}
}
\ No newline at end of file
/*
* 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.nachrichten.antragraum;
import static org.assertj.core.api.Assertions.*;
import java.time.format.DateTimeFormatter;
import org.junit.jupiter.api.Test;
import org.mapstruct.factory.Mappers;
import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory;
import de.ozgcloud.nachrichten.postfach.osi.MessageTestFactory;
class AntragraumNachrichtMapperTest {
AntragraumNachrichtMapper mapper = Mappers.getMapper(AntragraumNachrichtMapper.class);
@Test
void shouldMapVorgangId() {
var result = map();
assertThat(result.getVorgangId()).isEqualTo(MessageTestFactory.VORGANG_ID);
}
@Test
void shouldMapId() {
var result = map();
assertThat(result.getId()).isEqualTo(PostfachNachrichtTestFactory.ID);
}
@Test
void shouldMapVorgangName() {
var result = map();
assertThat(result.getVorgangName()).isEqualTo(MessageTestFactory.SUBJECT);
}
@Test
void shouldMapStatus() {
var result = map();
assertThat(result.getStatus()).isEqualTo(AntragraumNachrichtMapper.DEFAULT_STATUS);
}
@Test
void shouldMapSentAt() {
var result = map();
assertThat(result.getSentAt()).isEqualTo(PostfachNachrichtTestFactory.SENT_AT.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
}
@Test
void shouldMapText() {
var result = map();
assertThat(result.getText()).isEqualTo(PostfachNachrichtTestFactory.MAIL_BODY);
}
@Test
void shouldMapAttachments() {
var result = map();
assertThat(result.getAttachmentFileIdCount()).isEqualTo(1);
assertThat(result.getAttachmentFileId(0)).isEqualTo(PostfachNachrichtTestFactory.ATTACHMENTS.get(0));
}
private GrpcRueckfrage map() {
return mapper.toGrpc(PostfachNachrichtTestFactory.create());
}
}
\ No newline at end of file
package de.ozgcloud.nachrichten.antragraum;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
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.Response;
import de.ozgcloud.nachrichten.NachrichtenManagerProperties;
import de.ozgcloud.nachrichten.antragraum.AntragraumProperties;
import de.ozgcloud.nachrichten.antragraum.AntragraumService;
import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService;
class AntragraumServiceTest {
@Spy
@InjectMocks
private AntragraumService service;
......@@ -21,6 +23,14 @@ class AntragraumServiceTest {
private AntragraumProperties properties;
@Mock
private NachrichtenManagerProperties nachrichtenManagerProperties;
@Mock
private PersistPostfachNachrichtService postfachNachrichtService;
@Mock
private Saml2Verifier verifier;
@Mock
private Saml2Parser parser;
@Mock
private Saml2Decrypter decrypter;
@Nested
class TestGetAntragsraumUrl {
......@@ -38,4 +48,40 @@ class AntragraumServiceTest {
}
@Nested
class TestFindRueckfragen {
static final String SAML_TOKEN = "TOKEN";
@Test
void shouldCallVerify() {
service.findRueckfragen(SAML_TOKEN);
verify(service).verifyToken(anyString());
}
@Test
void shouldCallVerifier() {
service.findRueckfragen(SAML_TOKEN);
verify(verifier).verify(anyString());
}
@Test
void shouldCallDecrypt() {
when(parser.parse(anyString())).thenReturn(mock(Response.class));
service.findRueckfragen(SAML_TOKEN);
verify(decrypter).decryptPostfachId(any(Response.class));
}
@Test
void shouldCallPostfachService() {
when(decrypter.decryptPostfachId(any())).thenReturn(GrpcFindRueckfrageRequestTestFactory.POSTFACH_ID);
service.findRueckfragen(SAML_TOKEN);
verify(postfachNachrichtService).findRueckfragen(anyString());
}
}
}
\ No newline at end of file
/*
* 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.nachrichten.antragraum;
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.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.core.io.InputStreamResource;
import de.ozgcloud.common.test.TestUtils;
class BayernIdSamlConfigurationTest {
@Mock
private AntragraumProperties properties;
@InjectMocks
private BayernIdSamlConfiguration bayernIdSamlConfiguration;
@Nested
class TestCreationOfParserPool {
@BeforeEach
void setup() throws Exception {
bayernIdSamlConfiguration.initOpenSAML();
}
@Test
void shouldCreateParserPool() {
assertThat(bayernIdSamlConfiguration.getParserPool()).isNotNull();
}
}
@Nested
class TestCreatingTrustEngine {
@BeforeEach
void setup() throws Exception {
when(properties.getMetadataUri()).thenReturn(new InputStreamResource(TestUtils.loadFile("bayernid-idp-infra.xml")));
when(properties.getEntityId()).thenReturn("https://antragsraum.ozgcloud.de/");
bayernIdSamlConfiguration.initOpenSAML();
}
@Test
void shouldCreateTrustEngine() {
assertThat(bayernIdSamlConfiguration.getTrustEngine()).isNotNull();
}
}
@Nested
class TestLoadingVerficationData {
@Test
void shouldCreateVerificationCriteria() {
when(properties.getEntityId()).thenReturn("https://antragsraum.ozgcloud.de/");
bayernIdSamlConfiguration.initOpenSAML();
assertThat(bayernIdSamlConfiguration.getVerificationCriteria()).isNotNull();
}
@Test
void shouldLoadVerificationCert() throws Exception {
when(properties.getMetadataUri()).thenReturn(new InputStreamResource(TestUtils.loadFile("bayernid-idp-infra.xml")));
bayernIdSamlConfiguration.initOpenSAML();
assertThat(bayernIdSamlConfiguration.getCertificatesFromMetadata()).isNotNull();
}
}
}
\ No newline at end of file
/*
* 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.nachrichten.antragraum;
import de.ozgcloud.common.test.TestUtils;
import java.util.UUID;
public class GrpcFindRueckfrageRequestTestFactory {
static final String POSTFACH_ID = UUID.randomUUID().toString();
static final String SAML_TOKEN = TestUtils.loadTextFile("SamlResponse.xml");
static GrpcFindRueckfragenRequest create() {
return createBuilder().build();
}
static GrpcFindRueckfragenRequest.Builder createBuilder() {
return GrpcFindRueckfragenRequest.newBuilder()
.setPostfachId(POSTFACH_ID)
.setSamlToken(SAML_TOKEN);
}
}
/*
* 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.nachrichten.antragraum;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
import org.opensaml.saml.saml2.core.Response;
import org.springframework.core.io.InputStreamResource;
import org.springframework.test.util.ReflectionTestUtils;
import de.ozgcloud.common.test.TestUtils;
@Disabled("Passende certificat und key file muessen local vorhanden sein")
class Saml2DecrypterTest {
private Response samlResponse;
@Mock
private AntragraumProperties properties;
private BayernIdSamlConfiguration configuration;
private Saml2Decrypter decrypter;
@BeforeEach
void setup() throws Exception {
when(properties.getDecryptionCertificate()).thenReturn(new InputStreamResource(TestUtils.loadFile("bayernid-test-enc.crt")));
when(properties.getDecryptionPrivateKey()).thenReturn(new InputStreamResource(TestUtils.loadFile("bayernid-test-enc.key")));
configuration = new BayernIdSamlConfiguration();
ReflectionTestUtils.setField(configuration, "antragraumProperties", properties);
configuration.initOpenSAML();
var samlResponseString = TestUtils.loadTextFile("SamlResponse.xml");
var parser = new Saml2Parser(configuration);
samlResponse = parser.parse(samlResponseString);
decrypter = new Saml2Decrypter(configuration);
decrypter.init();
}
@Test
void shouldDecrypt() {
assertThat(samlResponse.getAssertions()).isEmpty();
decrypter.decryptResponseElements(samlResponse);
assertThat(samlResponse.getAssertions()).isNotEmpty();
}
@Test
void shouldHaveSubject() {
decrypter.decryptResponseElements(samlResponse);
var samlAssertion = samlResponse.getAssertions().get(0);
assertThat(samlAssertion.getSubject()).isNotNull();
}
@Test
void shouldHaveAuthnStatements() {
decrypter.decryptResponseElements(samlResponse);
var samlAssertion = samlResponse.getAssertions().get(0);
var authnStatements = samlAssertion.getAuthnStatements();
assertThat(authnStatements).isNotNull();
}
@Test
void shouldHaveStatements() {
decrypter.decryptResponseElements(samlResponse);
var samlAssertion = samlResponse.getAssertions().get(0);
var statements = samlAssertion.getStatements();
assertThat(statements).isNotNull();
}
@Test
void shouldHaveAttributes() {
decrypter.decryptResponseElements(samlResponse);
var samlAssertion = samlResponse.getAssertions().get(0);
var statements = (AttributeStatement) samlAssertion.getStatements().get(1);
var attributes = statements.getAttributes();
assertThat(attributes).hasSize(7);
}
@Test
void shouldHavePostfachId() {
decrypter.decryptResponseElements(samlResponse);
var samlAssertion = samlResponse.getAssertions().get(0);
var statements = (AttributeStatement) samlAssertion.getStatements().get(1);
var attributes = statements.getAttributes();
var postfachIdOptional = attributes.stream().filter(attribute -> "legacyPostkorbHandle".equals(attribute.getFriendlyName())).findFirst();
assertThat(postfachIdOptional).map(Attribute::getAttributeValues).isNotNull();
}
@Test
void shouldGetPostfachId() {
var postfachId = decrypter.decryptPostfachId(samlResponse);
assertThat(postfachId).isEqualTo("28721c6f-b78f-4d5c-a048-19fd2fc429d2");
}
}
\ No newline at end of file
package de.ozgcloud.nachrichten.antragraum;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opensaml.saml.saml2.core.Response;
import org.springframework.test.util.ReflectionTestUtils;
import de.ozgcloud.common.test.TestUtils;
class Saml2ParserTest {
private BayernIdSamlConfiguration configuration;
@BeforeEach
void setup() {
var properties = mock(AntragraumProperties.class);
configuration = new BayernIdSamlConfiguration();
ReflectionTestUtils.setField(configuration, "antragraumProperties", properties);
configuration.initOpenSAML();
}
@Test
void shouldInit() {
var parser = new Saml2Parser(configuration);
assertThat(parser).isNotNull();
}
@Test
void shouldParseSamlToken() throws Exception {
var response = getResponse();
assertThat(response).isNotNull();
}
@Test
void shouldHaveAssertions() throws Exception {
var response = getResponse();
assertThat(response.getAssertions()).isNotNull();
}
@Test
void shouldHaveEncryptedAssertions() throws Exception {
var response = getResponse();
assertThat(response.getEncryptedAssertions()).isNotNull();
}
@Test
void shouldHaveIssuer() throws Exception {
var response = getResponse();
assertThat(response.getIssuer().getValue()).isEqualTo("https://infra-pre-id.bayernportal.de/idp");
}
@Test
void shouldGetXMLObject() throws Exception {
var parser = new Saml2Parser(configuration);
try (var tokenStream = TestUtils.loadFile("SamlResponse.xml")) {
assertThat(parser.xmlObject(tokenStream)).isNotNull();
}
}
private Response getResponse() throws Exception {
var token = TestUtils.loadTextFile("SamlResponse.xml");
var parser = new Saml2Parser(configuration);
return parser.parse(token);
}
}
\ No newline at end of file
/*
* 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.nachrichten.antragraum;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.core.io.InputStreamResource;
import org.springframework.test.util.ReflectionTestUtils;
import de.ozgcloud.common.test.TestUtils;
class Saml2VerifierTest {
private String samlResponse;
private Saml2Verifier verifier;
@Mock
private AntragraumProperties properties;
private BayernIdSamlConfiguration configuration;
@BeforeEach
void setup() throws Exception {
when(properties.getMetadataUri()).thenReturn(new InputStreamResource(TestUtils.loadFile("bayernid-idp-infra.xml")));
when(properties.getEntityId()).thenReturn("https://antragsraum.ozgcloud.de");
samlResponse = TestUtils.loadTextFile("SamlResponse.xml");
configuration = new BayernIdSamlConfiguration();
ReflectionTestUtils.setField(configuration, "antragraumProperties", properties);
configuration.initOpenSAML();
var parser = new Saml2Parser(configuration);
verifier = new Saml2Verifier(parser, configuration);
verifier.init();
}
@Test
void shouldGetVerificationError() {
var res = verifier.verify(samlResponse);
assertThat(res).isNotEmpty();
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment