diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java index 0bbdf0ab26d16ad9f9c14f164f63f241995963f1..ee758e35fe044f497d04d2781b3fc10375575c7b 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java @@ -2,18 +2,27 @@ package de.ozgcloud.nachrichten.antragraum; import static java.util.Objects.*; +import java.util.stream.Stream; + import jakarta.annotation.PostConstruct; +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 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 +53,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)); + } + } + } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcService.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcService.java index 013b0f7e1ff09d930d2a03cac5426d3f9b0b6080..50bf5a243a16929fd93b3f54fbfac837943c0276 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcService.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcService.java @@ -20,29 +20,22 @@ package de.ozgcloud.nachrichten.antragraum; -import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService; +import org.apache.commons.lang3.NotImplementedException; + import de.ozgcloud.vorgang.grpc.command.GrpcCommand; import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.server.service.GrpcService; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.NotImplementedException; @GrpcService @RequiredArgsConstructor public class AntragsraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplBase { - private final PersistPostfachNachrichtService postfachNachrichtService; + private final AntragraumService antragraumService; private final AntragraumNachrichtMapper mapper; - private final Saml2Verifier verifier; - private final Saml2Parser parser; - private final Saml2Decrypter decrypter; @Override public void findRueckfragen(GrpcFindRueckfragenRequest request, StreamObserver<GrpcFindRueckfragenResponse> streamObserver) { - verifyToken(request.getSamlToken()); - String postfachId = decrypter.decryptPostfachId(parser.parse(request.getSamlToken())); - - var rueckfragen = postfachNachrichtService.findRueckfragen(postfachId).map(mapper::toGrpc).toList(); + var rueckfragen = antragraumService.findRueckfragen(request.getSamlToken()).map(mapper::toGrpc).toList(); var response = GrpcFindRueckfragenResponse.newBuilder().addAllRueckfrage(rueckfragen).build(); streamObserver.onNext(response); @@ -51,15 +44,7 @@ public class AntragsraumGrpcService extends AntragraumServiceGrpc.AntragraumServ @Override public void sendRueckfrageAnswer(GrpcSendRueckfrageAnswerRequest request, StreamObserver<GrpcCommand> streamObserver) { - verifyToken(request.getSamlToken()); - throw new NotImplementedException("sendRueckfrageAnswer not implemented yet"); } - void verifyToken(String token) { - var errors = verifier.verify(token); - if (CollectionUtils.isNotEmpty(errors)) { - throw new SecurityException("SAML Token verification failed. Errors: %s".formatted(errors)); - } - } } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdProperties.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..fca321c52f30d9075856970aec0b5d747da366a4 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdProperties.java @@ -0,0 +1,42 @@ +package de.ozgcloud.nachrichten.antragraum; + +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; +import lombok.Setter; + +@Getter +@Setter +@Configuration +@ConditionalOnProperty(AntragraumProperties.PROPERTY_ANTRAGSRAUM_URL) +@ConfigurationProperties(prefix = "ozgcloud.bayernid.saml") +@Validated +public class BayernIdProperties { + /** + * The entityId as defined the BayernId SAML Metadata + */ + @NotEmpty + private String entityId; + /** + * The uri where to load the idp Metadata from + */ + @NotEmpty + private Resource metadataUri; + /** + * The location of the private key for decrypting the saml token data + */ + @NotEmpty + private Resource decryptionPrivateKey; + /** + * The location of the cretificate for decrypting the saml token data + */ + @NotEmpty + private Resource decryptionCertificate; + +} diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfiguration.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfiguration.java index db27510dcee7d9c24325edca059dbc9ead771e03..10382dfbca35e94c38deb9bfda617ded5d427515 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfiguration.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfiguration.java @@ -20,23 +20,32 @@ package de.ozgcloud.nachrichten.antragraum; +import static de.ozgcloud.nachrichten.antragraum.Saml2Parser.*; + +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.HashSet; +import java.util.List; +import java.util.Set; + import jakarta.annotation.PostConstruct; -import lombok.extern.log4j.Log4j2; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; -import net.shibboleth.utilities.java.support.xml.XMLParserException; + 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.core.xml.io.Unmarshaller; 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.IDPSSODescriptor; import org.opensaml.saml.saml2.metadata.KeyDescriptor; import org.opensaml.security.credential.Credential; import org.opensaml.security.credential.CredentialResolver; @@ -50,180 +59,150 @@ 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.Value; -import org.springframework.context.annotation.Bean; 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 org.w3c.dom.Document; -import org.w3c.dom.Element; - -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.*; -import static de.ozgcloud.nachrichten.antragraum.Saml2Parser.*; +import lombok.RequiredArgsConstructor; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.xml.XMLParserException; +@RequiredArgsConstructor @Configuration -@Log4j2 public class BayernIdSamlConfiguration { - @Value("${ozgcloud.bayernid.saml.entity-id}") - private String entityId; - @Value("${ozgcloud.bayernid.saml.metadata-uri}") - private Resource metadataUri; - @Value("${ozgcloud.bayernid.saml.decryption.private-key-location}") - private Resource decryptionPrivateKey; - @Value("${ozgcloud.bayernid.saml.decryption.certificate-location}") - private Resource decryptionCertificate; - private XMLObjectProviderRegistry registry; - - @PostConstruct - void initOpenSAML() { - try { - registry = new XMLObjectProviderRegistry(); - ConfigurationService.register(XMLObjectProviderRegistry.class, registry); - - registry.setParserPool(getParserPool()); - InitializationService.initialize(); - } catch (Exception e) { - throw new RuntimeException("Initialization failed"); - } - } - - @Bean - Saml2Decrypter decrypter() { - return new Saml2Decrypter(asDecryptionCredential()); - } - - @Bean - Saml2Parser parser() { - return new Saml2Parser(); - } - - @Bean - Saml2Verifier verifier() { - return new Saml2Verifier(parser(), trustEngine(), verificationCriteria()); - } - - SignatureTrustEngine trustEngine() { - Set<Credential> credentials = new HashSet<>(); - Collection<Saml2X509Credential> keys = getCertificatesFromMetadata(); - - for (Saml2X509Credential key : keys) { - BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); - cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(entityId); - credentials.add(cred); - } - - CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); - return new ExplicitKeySignatureTrustEngine(credentialsResolver, DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); - } - - CriteriaSet verificationCriteria() { - CriteriaSet criteria = new CriteriaSet(); - criteria.add(new EvaluableEntityIDCredentialCriterion(new EntityIdCriterion(entityId))); - criteria.add(new EvaluableProtocolRoleDescriptorCriterion(new ProtocolCriterion("urn:oasis:names:tc:SAML:2.0:protocol"))); - criteria.add(new EvaluableUsageCredentialCriterion(new UsageCriterion(UsageType.SIGNING))); - return criteria; - } - - private Saml2X509Credential asDecryptionCredential() { - RSAPrivateKey privateKey = readPrivateKey(decryptionPrivateKey); - X509Certificate certificate = readCertificateFromResource(decryptionCertificate); - 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 (InputStream 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 (InputStream inputStream = location.getInputStream()) { - - return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(inputStream); - } catch (IOException | CertificateException e) { - throw new IllegalArgumentException(e); - } - } - - List<Saml2X509Credential> getCertificatesFromMetadata() { - try (InputStream metadata = metadataUri.getInputStream()) { - XMLObject 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 { - Document document = getParserPool().parse(inputStream); - Element element = document.getDocumentElement(); - Unmarshaller 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) { - IDPSSODescriptor 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)) { - List<X509Certificate> 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); - } - } + private final BayernIdProperties bayernIdProperties; + private XMLObjectProviderRegistry registry; + + @PostConstruct + void initOpenSAML() { + try { + registry = new XMLObjectProviderRegistry(); + ConfigurationService.register(XMLObjectProviderRegistry.class, registry); + + registry.setParserPool(getParserPool()); + InitializationService.initialize(); + } catch (Exception e) { + throw new RuntimeException("Initialization failed"); + } + } + + 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(bayernIdProperties.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(bayernIdProperties.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(bayernIdProperties.getDecryptionPrivateKey()); + var certificate = readCertificateFromResource(bayernIdProperties.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 = bayernIdProperties.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); + } + } } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Decrypter.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Decrypter.java index 62e6b589c6c99e5262cb92a8c57c415b07d5b4d2..41575e12f278e08dc763488fd80d0048e0c7d29c 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Decrypter.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Decrypter.java @@ -20,9 +20,13 @@ package de.ozgcloud.nachrichten.antragraum; -import org.opensaml.core.xml.XMLObject; +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.Assertion; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.Response; @@ -37,56 +41,59 @@ import org.opensaml.xmlsec.encryption.support.SimpleRetrievalMethodEncryptedKeyR import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver; import org.opensaml.xmlsec.keyinfo.impl.CollectionKeyInfoCredentialResolver; import org.springframework.security.saml2.Saml2Exception; -import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; +import lombok.RequiredArgsConstructor; +@Service +@RequiredArgsConstructor class Saml2Decrypter { - public static final String LEGACY_POSTKORB_HANDLE_KEY = "legacyPostkorbHandle"; - private final Decrypter decrypter; - private static final EncryptedKeyResolver encryptedKeyResolver = new ChainingEncryptedKeyResolver( - Arrays.asList(new InlineEncryptedKeyResolver(), new EncryptedElementTypeEncryptedKeyResolver(), - new SimpleRetrievalMethodEncryptedKeyResolver())); - - public Saml2Decrypter(Saml2X509Credential decryptionX509Credential) { - Collection<Credential> credentials = new ArrayList<>(); - - Credential cred = CredentialSupport.getSimpleCredential(decryptionX509Credential.getCertificate(), decryptionX509Credential.getPrivateKey()); - credentials.add(cred); - - KeyInfoCredentialResolver resolver = new CollectionKeyInfoCredentialResolver(credentials); - Decrypter decrypter = new Decrypter(null, resolver, encryptedKeyResolver); - decrypter.setRootInNewDocument(true); - - this.decrypter = decrypter; - } - - void decryptResponseElements(Response response) { - for (EncryptedAssertion encryptedAssertion : response.getEncryptedAssertions()) { - try { - Assertion 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 -> { - List<XMLObject> values = postfachIdAttribute.getAttributeValues(); - return ((XSString) values.get(0)).getValue(); - }).orElseThrow(); - - } + 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(); + + } } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Parser.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Parser.java index 21ebf47040a4f247cf733668b40a1377cf02db06..8c0a82f75231829d21d60085328f70d965f986b3 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Parser.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Parser.java @@ -20,84 +20,86 @@ package de.ozgcloud.nachrichten.antragraum; -import net.shibboleth.utilities.java.support.component.ComponentInitializationException; -import net.shibboleth.utilities.java.support.xml.BasicParserPool; -import net.shibboleth.utilities.java.support.xml.ParserPool; -import net.shibboleth.utilities.java.support.xml.XMLParserException; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.UnmarshallingException; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.impl.ResponseUnmarshaller; import org.springframework.security.saml2.Saml2Exception; -import org.w3c.dom.Document; -import org.w3c.dom.Element; +import org.springframework.stereotype.Service; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.xml.BasicParserPool; +import net.shibboleth.utilities.java.support.xml.ParserPool; +import net.shibboleth.utilities.java.support.xml.XMLParserException; +@Service class Saml2Parser { - private ParserPool parserPool; - private ResponseUnmarshaller unmarshaller; - - Saml2Parser() { - init(); - } - - void init() { - try { - this.parserPool = getParserPool(); - unmarshaller = (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory().getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - static ParserPool getParserPool() throws ComponentInitializationException { - BasicParserPool parserPool = new BasicParserPool(); - parserPool.setMaxPoolSize(100); - parserPool.setCoalescing(true); - parserPool.setIgnoreComments(true); - parserPool.setIgnoreElementContentWhitespace(true); - parserPool.setNamespaceAware(true); - parserPool.setExpandEntityReferences(false); - parserPool.setXincludeAware(false); - - final Map<String, Boolean> features = createFeatureMap(); - - parserPool.setBuilderFeatures(features); - - parserPool.setBuilderAttributes(new HashMap<>()); - - parserPool.initialize(); - - return parserPool; - } - - private static 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; - } - - Response parse(String request) { - return (Response) xmlObject(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); - } - - XMLObject xmlObject(InputStream inputStream) throws Saml2Exception { - try { - Document document = parserPool.parse(inputStream); - Element element = document.getDocumentElement(); - return unmarshaller.unmarshall(element); - } catch (XMLParserException | UnmarshallingException e) { - throw new Saml2Exception("Failed to deserialize LogoutRequest", e); - } - } + private ParserPool parserPool; + private ResponseUnmarshaller unmarshaller; + + Saml2Parser() { + init(); + } + + void init() { + try { + this.parserPool = getParserPool(); + unmarshaller = (ResponseUnmarshaller) XMLObjectProviderRegistrySupport.getUnmarshallerFactory() + .getUnmarshaller(Response.DEFAULT_ELEMENT_NAME); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static ParserPool getParserPool() throws ComponentInitializationException { + var parserPool = new BasicParserPool(); + parserPool.setMaxPoolSize(100); + parserPool.setCoalescing(true); + parserPool.setIgnoreComments(true); + parserPool.setIgnoreElementContentWhitespace(true); + parserPool.setNamespaceAware(true); + parserPool.setExpandEntityReferences(false); + parserPool.setXincludeAware(false); + + final var features = createFeatureMap(); + + parserPool.setBuilderFeatures(features); + + parserPool.setBuilderAttributes(new HashMap<>()); + + parserPool.initialize(); + + return parserPool; + } + + private static 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; + } + + Response parse(String request) { + return (Response) xmlObject(new ByteArrayInputStream(request.getBytes(StandardCharsets.UTF_8))); + } + + XMLObject xmlObject(InputStream inputStream) throws Saml2Exception { + try { + var document = parserPool.parse(inputStream); + var element = document.getDocumentElement(); + return unmarshaller.unmarshall(element); + } catch (XMLParserException | UnmarshallingException e) { + throw new Saml2Exception("Failed to deserialize LogoutRequest", e); + } + } } diff --git a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Verifier.java b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Verifier.java index 4377d65c97a3489bfe46c039842455c7a7d3493e..029f6a015c9cb3312588af52fcecad88881339e1 100644 --- a/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Verifier.java +++ b/src/main/java/de/ozgcloud/nachrichten/antragraum/Saml2Verifier.java @@ -20,54 +20,65 @@ package de.ozgcloud.nachrichten.antragraum; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +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.Signature; import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; +import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; @Log4j2 @RequiredArgsConstructor +@Service 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 SignatureTrustEngine trustEngine; - private final CriteriaSet verificationCriteria; + 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> verify(String samlToken) { + var response = parser.parse(samlToken); - List<Saml2Error> errors = new ArrayList<>(); - Signature signature = response.getSignature(); - SAMLSignatureProfileValidator 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()))); - } + 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)); - } + 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; - } + return errors; + } } diff --git a/src/main/resources/application-bayernId.yaml b/src/main/resources/application-bayernId.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8c90a64fc001ba9fdf129f03b842dc747592f18c --- /dev/null +++ b/src/main/resources/application-bayernId.yaml @@ -0,0 +1,8 @@ +ozgcloud: + bayernid: + saml: + entity-id: https://antragsraum.ozgcloud.de/ + metadata-uri: "classpath:/bayernid/metadata/bayernid-idp-infra.xml" + decryption: + private-key-location: "classpath:/bayernid/bayernid-test-enc.key" + certificate-location: "classpath:/bayernid/bayernid-test-enc.crt" \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8c90a64fc001ba9fdf129f03b842dc747592f18c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,8 +0,0 @@ -ozgcloud: - bayernid: - saml: - entity-id: https://antragsraum.ozgcloud.de/ - metadata-uri: "classpath:/bayernid/metadata/bayernid-idp-infra.xml" - decryption: - private-key-location: "classpath:/bayernid/bayernid-test-enc.key" - certificate-location: "classpath:/bayernid/bayernid-test-enc.crt" \ No newline at end of file diff --git a/src/main/resources/bayernid/metadata/bayernid-idp.xml b/src/main/resources/bayernid/metadata/bayernid-idp.xml deleted file mode 100644 index ebcc84c287b6dc26ed23c56a7ab359fc109f942e..0000000000000000000000000000000000000000 --- a/src/main/resources/bayernid/metadata/bayernid-idp.xml +++ /dev/null @@ -1,47 +0,0 @@ -<md:EntitiesDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"> - <md:EntityDescriptor entityID="https://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>MIIFbzCCA1egAwIBAgIJAIpEXcxsS1nEMA0GCSqGSIb3DQEBCwUAME4xCzAJBgNV - BAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMQ0wCwYD - VQQKDARBS0RCMQwwCgYDVQQLDANJRE0wHhcNMjAxMDI3MTMxNzE3WhcNMjUxMDI2 - MTMxNzE3WjBOMQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQH - DAhNdWVuY2hlbjENMAsGA1UECgwEQUtEQjEMMAoGA1UECwwDSURNMIICIjANBgkq - hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4MvmcEZhH4gcNhLLsEBhbB24fbU7ex1K - ZROJr4s8OMqAHryfenCFxxVKPI8Vm2+qg6SY+D+MbKHc3lITYecq0EAQQoV1M1bc - D31qVRtxTVb1Q9qGTv2V72LZB12qCCcgyWT4LOKTnGYUv5dIyj6EkEgVn8+Ia5Ow - Q5KeoCsh9yVo5PJBJXkerQEA/M4/LkT/obeaK+3+8eRGFmoGLb+bFM40ths9St2j - eeYT3UWl9NTY66mq4YVDZcaimUCMJQ4X9SjQ8HZeLgRzLx5X/6PwAJysyKVseN4j - SV+4sm/HnMrPcQLLLQCD0ltdp1jk5zvoZ6C8cZ8FJ7kCJ+Kvz1zEmH32Pw0bxPwB - QsKabyRxg5ESTIWwE2lWOnuooqSWk8lZIaGf8Sn8JFjRaVqVKN6JRFxgryurreJL - 9igN1e5cOdKYwXLfuqygcdVW40wYTjsL1MxKVf9Pb8J4EuOC95Hb85X6VckwRCx8 - Yroyr/UeDM8SKgsBJuSqTuFCo6oTf3zjDaD+BBzdVqBmQELfY81laG3at1A3vz0M - E9UkY5kv9/LH161YIDELgir63fBNsQSXPir5awP8xihgUlmCIfOXiMU+m9vp60KL - 8LzA2AODo2H5AY7BMxAlM+LF+KvPqUBxdq2QyTnhQFaeiP9l7Lk1xagnloiR6rW/ - GP+hsBAVO5UCAwEAAaNQME4wHQYDVR0OBBYEFAS91fX286nxHd+4kJSFE4clWiuC - MB8GA1UdIwQYMBaAFAS91fX286nxHd+4kJSFE4clWiuCMAwGA1UdEwQFMAMBAf8w - DQYJKoZIhvcNAQELBQADggIBAMt8FUGWNqtKAuVAklNojt9JXk476XvZNXn4lTjx - IaIyQLd+SpJ4vKPDAb78mKF4PtTdG2pqDIarvuxyLvG4Ut8Cd/NHTI9j9V7Yuw9g - MKPKzXfI9Zb4pOIantmEspRr/BApLPJzZMHI8KmeQ3V0Ip3i8QHMuHfkFuTN9hdS - mKTqoWZTo6dLawOkOpgjKh5RQjqjbXWUovMWQc/tffzUved0LePbPllJgSjUZRxm - +723ToZ1frFCYpFj1xJiOuqr+GJdtWz0DFRt+nXxRfznYtXd1YRC5BzpVwNtH8GL - OZfyH1HZtn1x5hcQOnRHwgcx37lJRW8yprAMiqLHtNYNlY/SD1KNMmqT07pkBwMq - Cgvic7T2yj9Jg38s6XjiwWiFRMNhC2+z9G1uf63tfXLi3J8HS8YyTSjEK+9aXMEj - 57HWHBsLAiAzO+RyrVdTEnp0f5qbUeu0d4DKvHocOWkxu0jp220ZlCHPm6f/nWM/ - Eo5TGT0htLo0vidoj36PmCu2KOJ+AhdEml81/M5ENg8ofhhkidiQo2he7eGjLyn4 - 4DPvSAiK/S1rvQmF65j/6mg0guFbGLpmLFQJXPztok2xTA+Vwvd/T5FxMNkKR9mh - P9E8rUGOu4hGB4KSHD8mo8nYyp1cCke72e7dMobhLGsCqxEnIeO+NTBzSbd/gqGm 2fd0 - </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://id.bayernportal.de/idp/profile/SAML2/POST/SSO"/> - <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - Location="https://id.bayernportal.de/idp/profile/SAML2/Redirect/SSO"/> - </md:IDPSSODescriptor> - </md:EntityDescriptor> -</md:EntitiesDescriptor> \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumNachrichtMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumNachrichtMapperTest.java index 3053701849da83da9c83a2dd886d620e1e9b9fa7..662034f677347ff99db12406bd7e9500d201c456 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumNachrichtMapperTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumNachrichtMapperTest.java @@ -20,12 +20,15 @@ package de.ozgcloud.nachrichten.antragraum; -import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; -import de.ozgcloud.nachrichten.postfach.osi.MessageTestFactory; +import static org.assertj.core.api.Assertions.*; + +import java.time.format.DateTimeFormatter; + import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; -import static org.assertj.core.api.Assertions.assertThat; +import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; +import de.ozgcloud.nachrichten.postfach.osi.MessageTestFactory; class AntragraumNachrichtMapperTest { @@ -63,7 +66,7 @@ class AntragraumNachrichtMapperTest { void shouldMapSendAt() { var result = map(); - assertThat(result.getSendAt()).isEqualTo(PostfachNachrichtTestFactory.SENT_AT.toString()); + assertThat(result.getSendAt()).isEqualTo(PostfachNachrichtTestFactory.SENT_AT.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } @Test diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumServiceTest.java index fb81269bda54f4e645399c75c7cf666bfaeaa11c..887e0f460bca95ca85d196645e9db1a88c9bf968 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumServiceTest.java @@ -1,19 +1,21 @@ 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 diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcServiceTest.java index 44852c4ee5fa3121500fb084af71ad2383ecde97..3aa2115e867a1e42201bd4eccb5b7bf20df5c440 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragsraumGrpcServiceTest.java @@ -20,79 +20,42 @@ package de.ozgcloud.nachrichten.antragraum; -import de.ozgcloud.nachrichten.postfach.PersistPostfachNachrichtService; -import de.ozgcloud.nachrichten.postfach.PostfachNachricht; -import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; -import io.grpc.stub.StreamObserver; +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.saml.saml2.core.Response; - -import java.util.stream.Stream; -import static org.mockito.Mockito.*; +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; +import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; +import io.grpc.stub.StreamObserver; class AntragsraumGrpcServiceTest { @Spy @InjectMocks private AntragsraumGrpcService antragsraumGrpcService; @Mock - private PersistPostfachNachrichtService postfachNachrichtService; + private AntragraumService antragraumService; @Mock private AntragraumNachrichtMapper mapper; - @Mock - private Saml2Verifier verifier; - @Mock - private Saml2Parser parser; - @Mock - private Saml2Decrypter decrypter; @Nested class TestFindRueckfragen { - @Mock - private StreamObserver<GrpcFindRueckfragenResponse> streamObserver; - - @Test - void shouldCallVerify() { - antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); - - verify(antragsraumGrpcService).verifyToken(anyString()); - } - - @Test - void shouldCallVerifier() { - antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); - - verify(verifier).verify(anyString()); - } - - @Test - void shouldCallDecrypt() { - when(parser.parse(anyString())).thenReturn(mock(Response.class)); - - antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); - - verify(decrypter).decryptPostfachId(any(Response.class)); - } - - @Test - void shouldCallPostfachService() { - when(decrypter.decryptPostfachId(any())).thenReturn(GrpcFindRueckfrageRequestTestFactory.POSTFACH_ID); - - antragsraumGrpcService.findRueckfragen(GrpcFindRueckfrageRequestTestFactory.create(), streamObserver); - - verify(postfachNachrichtService).findRueckfragen(anyString()); - } @Nested class TestFindRueckfragenGrpc { + @Mock + private StreamObserver<GrpcFindRueckfragenResponse> streamObserver; + @BeforeEach void setup() { - when(postfachNachrichtService.findRueckfragen(any())).thenReturn(Stream.of(PostfachNachrichtTestFactory.create())); + when(antragraumService.findRueckfragen(any())).thenReturn(Stream.of(PostfachNachrichtTestFactory.create())); when(mapper.toGrpc(any(PostfachNachricht.class))).thenReturn(GrpcRueckfrage.getDefaultInstance()); } diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationITCase.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationITCase.java deleted file mode 100644 index 711012919215c4d0e0e634b7a52e0614f5c0b339..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationITCase.java +++ /dev/null @@ -1,50 +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.nachrichten.antragraum; - -import de.ozgcloud.common.test.ITCase; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.*; - -@ITCase -@SpringBootTest(classes = {BayernIdSamlConfiguration.class}) -class BayernIdSamlConfigurationITCase { - @Autowired - private BayernIdSamlConfiguration bayernIdSamlConfiguration; - - @Test - void shouldCreateTrustEngine() { - assertThat(bayernIdSamlConfiguration.trustEngine()).isNotNull(); - } - - @Test - void shouldCreateVerificationCriteria() { - assertThat(bayernIdSamlConfiguration.verificationCriteria()).isNotNull(); - } - - @Test - void shouldLoadVerificationCert() throws Exception { - assertThat(bayernIdSamlConfiguration.getCertificatesFromMetadata()).isNotNull(); - } -} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..efe6f243454255e165261a248e631863d21f1a76 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/BayernIdSamlConfigurationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024. + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.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 BayernIdProperties properties; + @InjectMocks + private BayernIdSamlConfiguration bayernIdSamlConfiguration; + + @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 diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/GrpcNachrichtTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/GrpcNachrichtTestFactory.java index 1218f303b5e0c3dd25a8d11b30a7aef43adfb0af..cc500c7d3f9a12521445388101bd7c0d6af1a5bd 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/GrpcNachrichtTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/GrpcNachrichtTestFactory.java @@ -5,7 +5,7 @@ import de.ozgcloud.nachrichten.info.InfoManagerRequestTestFactory; import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory; import de.ozgcloud.nachrichten.postfach.osi.MessageTestFactory; -public class GrpcNachrichtTestFactory { +class GrpcNachrichtTestFactory { public static GrpcNachricht create() { return createBuilder().build(); diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterITCase.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterITCase.java deleted file mode 100644 index bd4861d04d28a886938bbdd15cd0d2001ffedaba..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterITCase.java +++ /dev/null @@ -1,112 +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.nachrichten.antragraum; - -import de.ozgcloud.common.test.ITCase; -import de.ozgcloud.common.test.TestUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.opensaml.saml.saml2.core.Attribute; -import org.opensaml.saml.saml2.core.AttributeStatement; -import org.opensaml.saml.saml2.core.Response; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.*; - -@ITCase -@SpringBootTest(classes = {BayernIdSamlConfiguration.class}) -public class Saml2DecrypterITCase { - private Response samlResponse; - - @Autowired - private Saml2Parser parser; - - @Autowired - private Saml2Decrypter decrypter; - - @BeforeEach - void setup() { - var samlResponseString = TestUtils.loadTextFile("SamlResponse.xml"); - samlResponse = parser.parse(samlResponseString); - } - - @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 diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5c5d511b49a113e4d43e146eafea8bcf41ca9835 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2DecrypterTest.java @@ -0,0 +1,121 @@ +/* + * 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.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 de.ozgcloud.common.test.TestUtils; + +class Saml2DecrypterTest { + private Response samlResponse; + + @Mock + private BayernIdProperties 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(properties); + configuration.initOpenSAML(); + + var samlResponseString = TestUtils.loadTextFile("SamlResponse.xml"); + samlResponse = new Saml2Parser().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 diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2ParserTest.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2ParserTest.java index bd64e2a3c9763c170bf06da60af712ba19c79057..c9efc057f7f8f41a945ba39bc864d8c9a93dbba0 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2ParserTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2ParserTest.java @@ -1,84 +1,66 @@ -/* - * 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 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 java.io.InputStream; - -import static org.assertj.core.api.Assertions.*; +import de.ozgcloud.common.test.TestUtils; class Saml2ParserTest { - @BeforeEach - void setup() { - new BayernIdSamlConfiguration().initOpenSAML(); - } + @BeforeEach + void setup() { + var properties = mock(BayernIdProperties.class); + new BayernIdSamlConfiguration(properties).initOpenSAML(); + } - @Test - void shouldInit() { - Saml2Parser parser = new Saml2Parser(); - assertThat(parser).isNotNull(); - } + @Test + void shouldInit() { + var parser = new Saml2Parser(); + assertThat(parser).isNotNull(); + } - @Test - void shouldParseSamlToken() throws Exception { - var response = getResponse(); - assertThat(response).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 shouldHaveAssertions() throws Exception { + var response = getResponse(); + assertThat(response.getAssertions()).isNotNull(); + } - @Test - void shouldHaveEncryptedAssertions() throws Exception { - var response = getResponse(); - assertThat(response.getEncryptedAssertions()).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 shouldHaveIssuer() throws Exception { + var response = getResponse(); + assertThat(response.getIssuer().getValue()).isEqualTo("https://infra-pre-id.bayernportal.de/idp"); + } - @Test - void shouldGetXMLObject() throws Exception { - Saml2Parser parser = new Saml2Parser(); + @Test + void shouldGetXMLObject() throws Exception { + var parser = new Saml2Parser(); - try (InputStream tokenStream = TestUtils.loadFile("SamlResponse.xml")) { - assertThat(parser.xmlObject(tokenStream)).isNotNull(); - } - } + try (var tokenStream = TestUtils.loadFile("SamlResponse.xml")) { + assertThat(parser.xmlObject(tokenStream)).isNotNull(); + } + } - private static Response getResponse() throws Exception { - var token = TestUtils.loadTextFile("SamlResponse.xml"); - Saml2Parser parser = new Saml2Parser(); //BayernIdSamlConfiguration.getParserPool() + private static Response getResponse() throws Exception { + var token = TestUtils.loadTextFile("SamlResponse.xml"); + var parser = new Saml2Parser(); - return parser.parse(token); - } + return parser.parse(token); + } } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierITCase.java b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierTest.java similarity index 51% rename from src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierITCase.java rename to src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierTest.java index f342a89b65c72aa3269c4c09d65e32cb931c4f27..7e94a28d5a8cef80aafc442389aa15da919513bc 100644 --- a/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierITCase.java +++ b/src/test/java/de/ozgcloud/nachrichten/antragraum/Saml2VerifierTest.java @@ -20,32 +20,43 @@ package de.ozgcloud.nachrichten.antragraum; -import de.ozgcloud.common.test.ITCase; -import de.ozgcloud.common.test.TestUtils; +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.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; +import org.mockito.Mock; +import org.springframework.core.io.InputStreamResource; -import static org.assertj.core.api.Assertions.*; +import de.ozgcloud.common.test.TestUtils; + +class Saml2VerifierTest { + private String samlResponse; + + private Saml2Verifier verifier; + + @Mock + private BayernIdProperties properties; + + private BayernIdSamlConfiguration configuration; -@ITCase -@SpringBootTest(classes = {BayernIdSamlConfiguration.class}) -public class Saml2VerifierITCase { - private String samlResponse; + @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"); - @Autowired - private Saml2Verifier verifier; + configuration = new BayernIdSamlConfiguration(properties); + configuration.initOpenSAML(); - @BeforeEach - void setup() { - samlResponse = TestUtils.loadTextFile("SamlResponse.xml"); - } + verifier = new Saml2Verifier(new Saml2Parser(), configuration); + verifier.init(); + } - @Test - void shouldGetVerificationError() { - var res = verifier.verify(samlResponse); + @Test + void shouldGetVerificationError() { + var res = verifier.verify(samlResponse); - assertThat(res).isNotEmpty(); - } + assertThat(res).isNotEmpty(); + } } \ No newline at end of file diff --git a/src/test/resources/application-bayernId.yaml b/src/test/resources/application-bayernId.yaml new file mode 100644 index 0000000000000000000000000000000000000000..6b68c25e625e679dbf6ce0cccb7d0c2cb7e94931 --- /dev/null +++ b/src/test/resources/application-bayernId.yaml @@ -0,0 +1,9 @@ +ozgcloud: + antragraum.url: "https://antragsraum.de" + bayernid: + saml: + entityid: https://antragsraum.ozgcloud.de/ + metadatauri: "classpath:/bayernid/metadata/bayernid-idp-infra.xml" + decryption: + private-key-location: "classpath:/bayernid/bayernid-test-enc.key" + certificate-location: "classpath:/bayernid/bayernid-test-enc.crt" \ No newline at end of file diff --git a/src/test/resources/bayernid-idp-infra.xml b/src/test/resources/bayernid-idp-infra.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec1ed7ca7099b8be7a8cff7448a740f0b9404c34 --- /dev/null +++ b/src/test/resources/bayernid-idp-infra.xml @@ -0,0 +1,45 @@ +<?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 diff --git a/src/test/resources/bayernid-test-enc.crt b/src/test/resources/bayernid-test-enc.crt new file mode 100644 index 0000000000000000000000000000000000000000..fe04bc8d5ad148f0e84ca1c98160c111f3aeba5d --- /dev/null +++ b/src/test/resources/bayernid-test-enc.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDsTCCApmgAwIBAgIUdw/27be5+2vj+MhGtoJjDsMsdDEwDQYJKoZIhvcNAQEL +BQAwaDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVl +bmNoZW4xDzANBgNVBAoMBm1nbSB0cDEkMCIGCSqGSIb3DQEJARYVamVucy5yZWVz +ZUBtZ20tdHAuY29tMB4XDTI0MDExNjEyMjI0OVoXDTI1MDExNTEyMjI0OVowaDEL +MAkGA1UEBhMCREUxDzANBgNVBAgMBkJheWVybjERMA8GA1UEBwwITXVlbmNoZW4x +DzANBgNVBAoMBm1nbSB0cDEkMCIGCSqGSIb3DQEJARYVamVucy5yZWVzZUBtZ20t +dHAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/HBBWBDSrEgd +wXkSy15V00EaVTyLgc4vh/JcDiGIYZSqmcMwBd+B1u36xbdBf/duEtCUymMNP48O +MjgFZtR6xn0meuR4NR6Ykn9mYGdU/GhldGuGv9XLAEAkVuTlo0H1QYyBS/6JwKQo +SsHDkJ3YwDwKcyOt7QtpSadRZjQEN3gDvWoRYjgXTxj2I1ovllmi0zOHsFi5PBIu +iPWUdJvBrHxpD/XVS9R/qzJpHPu3bjQ6UVRmhiZCUF7H5F/PQNwk+qXvjV0ooBeS +WWO5hywhk4OP4QEgbYMOSo20YukYX8TJEsum1pwIcQrw7kW4GyKaAycyRsa1fbM3 +tEkj+TiBKwIDAQABo1MwUTAdBgNVHQ4EFgQUfDL/6R33SJodsONCvxKy96AtU18w +HwYDVR0jBBgwFoAUfDL/6R33SJodsONCvxKy96AtU18wDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEA+PCnvSwKU+bArTCIg5lfrwONbzKkjvPUymDN +YX3oj1wVEN75hNf0RD7Rr0//ZYT3Rt0G193gjDcH1gbGIYhMLeGGkxEous2l3O+p +RIQRR+hprjr6HzF8IphaJy1RbDwyGsXyLcyOylPL4cX9IjUdhklHiLZusBq95LSy +w7hsCOAL2+vn816O7yv+28EWXXbnP2XEUjW36nxcZvR6oTJUplXyHRuuJJTsOxGR +NuXA3UVgNbkdm1HnoSGpnsGdUKsUFoEmEJkcSdQRwxeH21WzYGOZmKMcvx2gObaS +P8tafWh5z4Jx+Z7z5WP72Jt44/lnVjaV8aGo0KHXwgqQOtYftQ== +-----END CERTIFICATE----- diff --git a/src/test/resources/bayernid-test-enc.key b/src/test/resources/bayernid-test-enc.key new file mode 100644 index 0000000000000000000000000000000000000000..8d1c8b69c3fce7bea45c73efd06983e3c419a92f --- /dev/null +++ b/src/test/resources/bayernid-test-enc.key @@ -0,0 +1 @@ +