diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadata.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadata.java new file mode 100644 index 0000000000..c75de010d4 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadata.java @@ -0,0 +1,276 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +import org.springframework.security.saml2.core.Saml2X509Credential; + +/** + * An interface representing SAML 2.0 Asserting Party metadata + * + * @author Josh Cummings + * @since 6.4 + */ +public interface AssertingPartyMetadata { + + /** + * Get the asserting party's EntityID. + * + *

+ * Equivalent to the value found in the asserting party's <EntityDescriptor + * EntityID="..."/> + * + *

+ * This value may contain a number of placeholders, which need to be resolved before + * use. They are {@code baseUrl}, {@code registrationId}, {@code baseScheme}, + * {@code baseHost}, and {@code basePort}. + * @return the asserting party's EntityID + */ + String getEntityId(); + + /** + * Get the WantAuthnRequestsSigned setting, indicating the asserting party's + * preference that relying parties should sign the AuthnRequest before sending. + * @return the WantAuthnRequestsSigned value + */ + boolean getWantAuthnRequestsSigned(); + + /** + * Get the list of org.opensaml.saml.ext.saml2alg.SigningMethod Algorithms for this + * asserting party, in preference order. + * + *

+ * Equivalent to the values found in <SigningMethod Algorithm="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the list of SigningMethod Algorithms + * @since 5.5 + */ + List getSigningAlgorithms(); + + /** + * Get all verification {@link Saml2X509Credential}s associated with this asserting + * party + * @return all verification {@link Saml2X509Credential}s associated with this + * asserting party + * @since 5.4 + */ + Collection getVerificationX509Credentials(); + + /** + * Get all encryption {@link Saml2X509Credential}s associated with this asserting + * party + * @return all encryption {@link Saml2X509Credential}s associated with this asserting + * party + * @since 5.4 + */ + Collection getEncryptionX509Credentials(); + + /** + * Get the SingleSignOnService + * Location. + * + *

+ * Equivalent to the value found in <SingleSignOnService Location="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the SingleSignOnService Location + */ + String getSingleSignOnServiceLocation(); + + /** + * Get the SingleSignOnService + * Binding. + * + *

+ * Equivalent to the value found in <SingleSignOnService Binding="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the SingleSignOnService Location + */ + Saml2MessageBinding getSingleSignOnServiceBinding(); + + /** + * Get the SingleLogoutService + * Location + * + *

+ * Equivalent to the value found in <SingleLogoutService Location="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Location + * @since 5.6 + */ + String getSingleLogoutServiceLocation(); + + /** + * Get the SingleLogoutService + * Response Location + * + *

+ * Equivalent to the value found in <SingleLogoutService Location="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Response Location + * @since 5.6 + */ + String getSingleLogoutServiceResponseLocation(); + + /** + * Get the SingleLogoutService + * Binding + * + *

+ * Equivalent to the value found in <SingleLogoutService Binding="..."/> in the + * asserting party's <IDPSSODescriptor>. + * @return the SingleLogoutService Binding + * @since 5.6 + */ + Saml2MessageBinding getSingleLogoutServiceBinding(); + + Builder mutate(); + + interface Builder> { + + /** + * Set the asserting party's EntityID. + * Equivalent to the value found in the asserting party's <EntityDescriptor + * EntityID="..."/> + * @param entityId the asserting party's EntityID + * @return the {@link B} for further configuration + */ + B entityId(String entityId); + + /** + * Set the WantAuthnRequestsSigned setting, indicating the asserting party's + * preference that relying parties should sign the AuthnRequest before sending. + * @param wantAuthnRequestsSigned the WantAuthnRequestsSigned setting + * @return the {@link B} for further configuration + */ + B wantAuthnRequestsSigned(boolean wantAuthnRequestsSigned); + + /** + * Apply this {@link Consumer} to the list of SigningMethod Algorithms + * @param signingMethodAlgorithmsConsumer a {@link Consumer} of the list of + * SigningMethod Algorithms + * @return this {@link B} for further configuration + * @since 5.5 + */ + B signingAlgorithms(Consumer> signingMethodAlgorithmsConsumer); + + /** + * Apply this {@link Consumer} to the list of {@link Saml2X509Credential}s + * @param credentialsConsumer a {@link Consumer} of the {@link List} of + * {@link Saml2X509Credential}s + * @return the {@link RelyingPartyRegistration.Builder} for further configuration + * @since 5.4 + */ + B verificationX509Credentials(Consumer> credentialsConsumer); + + /** + * Apply this {@link Consumer} to the list of {@link Saml2X509Credential}s + * @param credentialsConsumer a {@link Consumer} of the {@link List} of + * {@link Saml2X509Credential}s + * @return the {@link RelyingPartyRegistration.Builder} for further configuration + * @since 5.4 + */ + B encryptionX509Credentials(Consumer> credentialsConsumer); + + /** + * Set the SingleSignOnService + * Location. + * + *

+ * Equivalent to the value found in <SingleSignOnService Location="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @param singleSignOnServiceLocation the SingleSignOnService Location + * @return the {@link B} for further configuration + */ + B singleSignOnServiceLocation(String singleSignOnServiceLocation); + + /** + * Set the SingleSignOnService + * Binding. + * + *

+ * Equivalent to the value found in <SingleSignOnService Binding="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @param singleSignOnServiceBinding the SingleSignOnService Binding + * @return the {@link B} for further configuration + */ + B singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServiceBinding); + + /** + * Set the SingleLogoutService + * Location + * + *

+ * Equivalent to the value found in <SingleLogoutService Location="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @param singleLogoutServiceLocation the SingleLogoutService Location + * @return the {@link B} for further configuration + * @since 5.6 + */ + B singleLogoutServiceLocation(String singleLogoutServiceLocation); + + /** + * Set the SingleLogoutService + * Response Location + * + *

+ * Equivalent to the value found in <SingleLogoutService + * ResponseLocation="..."/> in the asserting party's <IDPSSODescriptor>. + * @param singleLogoutServiceResponseLocation the SingleLogoutService Response + * Location + * @return the {@link B} for further configuration + * @since 5.6 + */ + B singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation); + + /** + * Set the SingleLogoutService + * Binding + * + *

+ * Equivalent to the value found in <SingleLogoutService Binding="..."/> in + * the asserting party's <IDPSSODescriptor>. + * @param singleLogoutServiceBinding the SingleLogoutService Binding + * @return the {@link B} for further configuration + * @since 5.6 + */ + B singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding); + + /** + * Creates an immutable ProviderDetails object representing the configuration for + * an Identity Provider, IDP + * @return immutable ProviderDetails object + */ + AssertingPartyMetadata build(); + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadataRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadataRepository.java new file mode 100644 index 0000000000..d03cea8c8c --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/AssertingPartyMetadataRepository.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.provider.service.registration; + +import org.springframework.lang.Nullable; + +/** + * A repository for retrieving SAML 2.0 Asserting Party Metadata + * + * @author Josh Cummings + * @since 6.4 + * @see OpenSamlAssertingPartyMetadataRepository + * @see org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations + */ +public interface AssertingPartyMetadataRepository extends Iterable { + + /** + * Retrieve an {@link AssertingPartyMetadata} by its EntityID. + * @param entityId the EntityID to lookup + * @return the found {@link AssertingPartyMetadata}, or {@code null} otherwise + */ + @Nullable + default AssertingPartyMetadata findByEntityId(String entityId) { + for (AssertingPartyMetadata metadata : this) { + if (metadata.getEntityId().equals(entityId)) { + return metadata; + } + } + return null; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java index 87cfea754e..56f1c9e7c1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java @@ -338,6 +338,25 @@ public class RelyingPartyRegistration { return new Builder(assertingPartyDetails.getEntityId(), assertingPartyDetails.mutate()); } + /** + * Creates a {@code RelyingPartyRegistration} {@link Builder} with a + * {@code registrationId} equivalent to the asserting party entity id. Also + * initializes to the contents of the given {@link AssertingPartyMetadata}. + * + *

+ * Presented as a convenience method when working with + * {@link AssertingPartyMetadataRepository} return values. As such, only supports + * {@link AssertingPartyMetadata} instances of type {@link AssertingPartyDetails}. + * @param metadata the metadata used to initialize the + * {@link RelyingPartyRegistration} {@link Builder} + * @return {@link Builder} to create a {@link RelyingPartyRegistration} object + * @since 6.4 + */ + public static Builder withAssertingPartyMetadata(AssertingPartyMetadata metadata) { + Assert.isInstanceOf(AssertingPartyDetails.class, metadata, "metadata must be of type AssertingPartyDetails"); + return withAssertingPartyDetails((AssertingPartyDetails) metadata); + } + /** * Creates a {@code RelyingPartyRegistration} {@link Builder} based on an existing * object @@ -380,7 +399,7 @@ public class RelyingPartyRegistration { * * @since 5.4 */ - public static class AssertingPartyDetails { + public static class AssertingPartyDetails implements AssertingPartyMetadata { private final String entityId; @@ -584,7 +603,7 @@ public class RelyingPartyRegistration { .singleLogoutServiceBinding(this.singleLogoutServiceBinding); } - public static class Builder { + public static class Builder implements AssertingPartyMetadata.Builder { private String entityId; diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java index f85320e932..04c6f79f1a 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationTests.java @@ -166,4 +166,30 @@ public class RelyingPartyRegistrationTests { .containsExactly(encryptingCredential, altApCredential); } + @Test + void withAssertingPartyMetadataWhenDetailsThenBuilderCopies() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .nameIdFormat("format") + .assertingPartyDetails((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) + .assertingPartyDetails((a) -> a.wantAuthnRequestsSigned(false)) + .assertingPartyDetails((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + RelyingPartyRegistration copied = RelyingPartyRegistration + .withAssertingPartyMetadata(registration.getAssertingPartyDetails()) + .registrationId(registration.getRegistrationId()) + .entityId(registration.getEntityId()) + .signingX509Credentials((c) -> c.addAll(registration.getSigningX509Credentials())) + .decryptionX509Credentials((c) -> c.addAll(registration.getDecryptionX509Credentials())) + .assertionConsumerServiceLocation(registration.getAssertionConsumerServiceLocation()) + .assertionConsumerServiceBinding(registration.getAssertionConsumerServiceBinding()) + .singleLogoutServiceLocation(registration.getSingleLogoutServiceLocation()) + .singleLogoutServiceResponseLocation(registration.getSingleLogoutServiceResponseLocation()) + .singleLogoutServiceBindings((c) -> c.addAll(registration.getSingleLogoutServiceBindings())) + .nameIdFormat(registration.getNameIdFormat()) + .authnRequestsSigned(registration.isAuthnRequestsSigned()) + .build(); + compareRegistrations(registration, copied); + } + }