diff --git a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java index 60fe23c43ec..30274206b5c 100644 --- a/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/saml2/RelyingPartyRegistrationsBeanDefinitionParser.java @@ -39,6 +39,7 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.security.converter.RsaKeyConverters; import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.provider.service.registration.AssertingPartyMetadata; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations; @@ -153,7 +154,7 @@ private static Map> getAssertingParties(Element elem } private static void addVerificationCredentials(Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { List verificationCertificateLocations = (List) assertingParty.get(ELT_VERIFICATION_CREDENTIAL); List verificationCredentials = new ArrayList<>(); for (String certificateLocation : verificationCertificateLocations) { @@ -163,7 +164,7 @@ private static void addVerificationCredentials(Map assertingPart } private static void addEncryptionCredentials(Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { List encryptionCertificateLocations = (List) assertingParty.get(ELT_ENCRYPTION_CREDENTIAL); List encryptionCredentials = new ArrayList<>(); for (String certificateLocation : encryptionCertificateLocations) { @@ -220,8 +221,8 @@ private static RelyingPartyRegistration.Builder getBuilderFromMetadataLocationIf } else { builder = RelyingPartyRegistration.withRegistrationId(registrationId) - .assertingPartyDetails((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt, assertingParties, - apBuilder, parserContext)); + .assertingPartyMetadata((apBuilder) -> buildAssertingParty(relyingPartyRegistrationElt, + assertingParties, apBuilder, parserContext)); } addRemainingProperties(relyingPartyRegistrationElt, builder); return builder; @@ -260,7 +261,7 @@ private static void addRemainingProperties(Element relyingPartyRegistrationElt, } private static void buildAssertingParty(Element relyingPartyElt, Map> assertingParties, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder, ParserContext parserContext) { + AssertingPartyMetadata.Builder builder, ParserContext parserContext) { String assertingPartyId = relyingPartyElt.getAttribute(ATT_ASSERTING_PARTY_ID); if (!assertingParties.containsKey(assertingPartyId)) { Object source = parserContext.extractSource(relyingPartyElt); @@ -293,7 +294,7 @@ private static void buildAssertingParty(Element relyingPartyElt, Map assertingParty, - RelyingPartyRegistration.AssertingPartyDetails.Builder builder) { + AssertingPartyMetadata.Builder builder) { String signingAlgorithmsAttr = getAsString(assertingParty, ATT_SIGNING_ALGORITHMS); if (StringUtils.hasText(signingAlgorithmsAttr)) { List signingAlgorithms = Arrays.asList(signingAlgorithmsAttr.split(",")); diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index 6e4e49d891f..34ca0d8bc71 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -114,7 +114,7 @@ Java:: ---- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") // ... - .assertingPartyDetails(party -> party + .assertingPartyMetadata(party -> party // ... .wantAuthnRequestsSigned(false) ) @@ -128,7 +128,7 @@ Kotlin:: var relyingPartyRegistration: RelyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") // ... - .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party + .assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party // ... .wantAuthnRequestsSigned(false) } @@ -154,7 +154,7 @@ Java:: String metadataLocation = "classpath:asserting-party-metadata.xml"; RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) // ... - .assertingPartyDetails((party) -> party + .assertingPartyMetadata((party) -> party // ... .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512)) ) @@ -169,7 +169,7 @@ var metadataLocation = "classpath:asserting-party-metadata.xml" var relyingPartyRegistration: RelyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation) // ... - .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party + .assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party // ... .signingAlgorithms { sign: MutableList -> sign.add( @@ -197,7 +197,7 @@ Java:: ---- RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta") // ... - .assertingPartyDetails(party -> party + .assertingPartyMetadata(party -> party // ... .singleSignOnServiceBinding(Saml2MessageBinding.POST) ) @@ -211,7 +211,7 @@ Kotlin:: var relyingPartyRegistration: RelyingPartyRegistration? = RelyingPartyRegistration.withRegistrationId("okta") // ... - .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party + .assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party // ... .singleSignOnServiceBinding(Saml2MessageBinding.POST) } diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index 77c43da2dc1..d86eeb2c565 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -484,7 +484,7 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exc Saml2X509Credential credential = Saml2X509Credential.verification(certificate); RelyingPartyRegistration registration = RelyingPartyRegistration .withRegistrationId("example") - .assertingPartyDetails(party -> party + .assertingPartyMetadata(party -> party .entityId("https://idp.example.com/issuer") .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2") .wantAuthnRequestsSigned(false) @@ -508,7 +508,7 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository { val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate) val registration = RelyingPartyRegistration .withRegistrationId("example") - .assertingPartyDetails { party: AssertingPartyDetails.Builder -> + .assertingPartyMetadata { party: AssertingPartyMetadata.Builder -> party .entityId("https://idp.example.com/issuer") .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2") @@ -699,7 +699,7 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit .entityId("{baseUrl}/{registrationId}") .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential())) .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}") - .assertingPartyDetails(party -> party + .assertingPartyMetadata(party -> party .entityId("https://ap.example.org") .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential())) .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2") @@ -718,7 +718,7 @@ val relyingPartyRegistration = c.add(relyingPartyDecryptingCredential()) } .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}") - .assertingPartyDetails { party -> party + .assertingPartyMetadata { party -> party .entityId("https://ap.example.org") .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) } .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2") @@ -730,7 +730,7 @@ val relyingPartyRegistration = [TIP] ==== The top-level metadata methods are details about the relying party. -The methods inside `assertingPartyDetails` are details about the asserting party. +The methods inside `AssertingPartyMetadata` are details about the asserting party. ==== [NOTE] diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 97e155cdd30..91aeb651aec 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -339,7 +339,7 @@ It's common to need to set other values in the `` than the By default, Spring Security will issue a `` and supply: -* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation` +* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation` * The `ID` attribute - a GUID * The `` element - from `RelyingPartyRegistration#getEntityId` * The `` element - from `Authentication#getName` @@ -424,7 +424,7 @@ It's common to need to set other values in the `` than the By default, Spring Security will issue a `` and supply: -* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation` +* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation` * The `ID` attribute - a GUID * The `` element - from `RelyingPartyRegistration#getEntityId` * The `` element - `SUCCESS` diff --git a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc index 4dd00440a12..ac9b6970138 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc @@ -1,14 +1,14 @@ [[servlet-saml2login-metadata]] = Saml 2.0 Metadata -Spring Security can <> to produce an `AssertingPartyDetails` instance as well as <> from a `RelyingPartyRegistration` instance. +Spring Security can <> to produce an `AssertingPartyMetadata` instance as well as <> from a `RelyingPartyRegistration` instance. [[parsing-asserting-party-metadata]] == Parsing `` metadata You can parse an asserting party's metadata xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistrationrepository[using `RelyingPartyRegistrations`]. -When using the OpenSAML vendor support, the resulting `AssertingPartyDetails` will be of type `OpenSamlAssertingPartyDetails`. +When using the OpenSAML vendor support, the resulting `AssertingPartyMetadata` will be of type `OpenSamlAssertingPartyDetails`. This means you'll be able to do get the underlying OpenSAML XMLObject by doing the following: [tabs] @@ -18,7 +18,7 @@ Java:: [source,java,role="primary"] ---- OpenSamlAssertingPartyDetails details = (OpenSamlAssertingPartyDetails) - registration.getAssertingPartyDetails(); + registration.getAssertingPartyMetadata(); EntityDescriptor openSamlEntityDescriptor = details.getEntityDescriptor(); ---- @@ -27,11 +27,129 @@ Kotlin:: [source,kotlin,role="secondary"] ---- val details: OpenSamlAssertingPartyDetails = - registration.getAssertingPartyDetails() as OpenSamlAssertingPartyDetails; -val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor(); + registration.getAssertingPartyMetadata() as OpenSamlAssertingPartyDetails +val openSamlEntityDescriptor: EntityDescriptor = details.getEntityDescriptor() ---- ====== +=== Using `AssertingPartyMetadataRepository` + +You can also be more targeted than `RelyingPartyRegistrations` by using `AssertingPartyMetadataRepository`, an interface that allows for only retrieving the asserting party metadata. + +This allows three valuable features: + +* Implementations can refresh asserting party metadata in an expiry-aware fashion +* Implementations of `RelyingPartyRegistrationRepository` can more easily articulate a relationship between a relying party and its one or many corresponding asserting parties +* Implementations can verify metadata signatures + +For example, `OpenSamlAssertingPartyMetadataRepository` uses OpenSAML's `MetadataResolver`, and API whose implementations regularly refresh the underlying metadata in an expiry-aware fashion. + +This means that you can now create a refreshable `RelyingPartyRegistrationRepository` in just a few lines of code: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Component +public class RefreshableRelyingPartyRegistrationRepository + implements IterableRelyingPartyRegistrationRepository { + + private final AssertingPartyMetadataRepository metadata = + OpenSamlAssertingPartyMetadataRepository + .fromTrustedMetadataLocation("https://idp.example.org/metadata").build(); + + @Override + public RelyingPartyRegistration findByRegistrationId(String registrationId) { + AssertingPartyMetadata metadata = this.metadata.findByEntityId(registrationId); + if (metadata == null) { + return null; + } + return applyRelyingParty(metadata); + } + + @Override + public Iterator iterator() { + return StreamSupport.stream(this.metadata.spliterator(), false) + .map(this::applyRelyingParty).iterator(); + } + + private RelyingPartyRegistration applyRelyingParty(AssertingPartyMetadata metadata) { + return RelyingPartyRegistration.withAssertingPartyMetadata(metadata) + // apply any relying party configuration + .build(); + } + +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +class RefreshableRelyingPartyRegistrationRepository : IterableRelyingPartyRegistrationRepository { + + private val metadata: AssertingPartyMetadataRepository = + OpenSamlAssertingPartyMetadataRepository.fromTrustedMetadataLocation( + "https://idp.example.org/metadata").build() + + fun findByRegistrationId(registrationId:String?): RelyingPartyRegistration { + val metadata = this.metadata.findByEntityId(registrationId) + if (metadata == null) { + return null + } + return applyRelyingParty(metadata) + } + + fun iterator(): Iterator { + return StreamSupport.stream(this.metadata.spliterator(), false) + .map(this::applyRelyingParty).iterator() + } + + private fun applyRelyingParty(metadata: AssertingPartyMetadata): RelyingPartyRegistration { + val details: AssertingPartyMetadata = metadata as AssertingPartyMetadata + return RelyingPartyRegistration.withAssertingPartyMetadata(details) + // apply any relying party configuration + .build() + } + } +---- +====== + +[TIP] +`OpenSamlAssertingPartyMetadataRepository` also ships with a constructor so you can provide a custom `MetadataResolver`. Since the underlying `MetadataResolver` is doing the expirying and refreshing, if you use the constructor directly, you will only get these features by providing an implementation that does so. + +=== Verifying Metadata Signatures + +You can also verify metadata signatures using `OpenSamlAssertingPartyMetadataRepository` by providing the appropriate set of ``Saml2X509Credential``s as follows: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +OpenSamlAssertingPartyMetadataRepository.withMetadataLocation("https://idp.example.org/metadata") + .verificationCredentials((c) -> c.add(myVerificationCredential)) + .build(); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +OpenSamlAssertingPartyMetadataRepository.withMetadataLocation("https://idp.example.org/metadata") + .verificationCredentials({ c : Collection -> + c.add(myVerificationCredential) }) + .build() +---- +====== + +[NOTE] +If no credentials are provided, the component will not perform signature validation. + [[publishing-relying-party-metadata]] == Producing `` Metadata diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java index dbb5bc464a1..a2a390b1bb7 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProvider.java @@ -400,7 +400,7 @@ public static Converter createDefau result = result.concat(new Saml2Error(Saml2ErrorCodes.INVALID_DESTINATION, message)); } String assertingPartyEntityId = token.getRelyingPartyRegistration() - .getAssertingPartyDetails() + .getAssertingPartyMetadata() .getEntityId(); if (!StringUtils.hasText(issuer) || !issuer.equals(assertingPartyEntityId)) { String message = String.format("Invalid issuer [%s] for SAML response [%s]", issuer, response.getID()); @@ -775,7 +775,7 @@ private static ValidationContext createValidationContext(AssertionToken assertio RelyingPartyRegistration relyingPartyRegistration = token.getRelyingPartyRegistration(); String audience = relyingPartyRegistration.getEntityId(); String recipient = relyingPartyRegistration.getAssertionConsumerServiceLocation(); - String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyDetails().getEntityId(); + String assertingPartyEntityId = relyingPartyRegistration.getAssertingPartyMetadata().getEntityId(); Map params = new HashMap<>(); Assertion assertion = assertionToken.getAssertion(); if (assertionContainsInResponseTo(assertion)) { diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java index 67910737c6f..4861ebce7e1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlSigningUtils.java @@ -96,7 +96,7 @@ static QueryParametersPartial sign(RelyingPartyRegistration registration) { private static SignatureSigningParameters resolveSigningParameters( RelyingPartyRegistration relyingPartyRegistration) { List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java index b581501a893..93928202a23 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/OpenSamlVerificationUtils.java @@ -73,11 +73,12 @@ static VerifierPartial verifySignature(RequestAbstractType object, RelyingPartyR static SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { Set credentials = new HashSet<>(); - Collection keys = registration.getAssertingPartyDetails().getVerificationX509Credentials(); + Collection keys = registration.getAssertingPartyMetadata() + .getVerificationX509Credentials(); for (Saml2X509Credential key : keys) { BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + cred.setEntityId(registration.getAssertingPartyMetadata().getEntityId()); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java index dbf348ebe54..d0fb791970a 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2PostAuthenticationRequest.java @@ -50,7 +50,7 @@ public Saml2MessageBinding getBinding() { * @since 5.7 */ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { - String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); + String location = registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation(); return new Builder(registration).authenticationRequestUri(location); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java index 8dd6589962d..4101801204f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/Saml2RedirectAuthenticationRequest.java @@ -73,7 +73,7 @@ public Saml2MessageBinding getBinding() { * @since 5.7 */ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration registration) { - String location = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); + String location = registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation(); return new Builder(registration).authenticationRequestUri(location); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java index 00875571a39..061c97d00aa 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutRequestValidator.java @@ -134,7 +134,7 @@ private Consumer> validateIssuer(LogoutRequest request, return; } String issuer = request.getIssuer().getValue(); - if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) { + if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) { errors .add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java index b718bd04e64..64041583b2f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlLogoutResponseValidator.java @@ -132,7 +132,7 @@ private Consumer> validateIssuer(LogoutResponse response, return; } String issuer = response.getIssuer().getValue(); - if (!issuer.equals(registration.getAssertingPartyDetails().getEntityId())) { + if (!issuer.equals(registration.getAssertingPartyMetadata().getEntityId())) { errors .add(new Saml2Error(Saml2ErrorCodes.INVALID_ISSUER, "Failed to match issuer to configured issuer")); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java index 0601b0bc6d8..e0da9859310 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/OpenSamlVerificationUtils.java @@ -164,12 +164,12 @@ private CriteriaSet verificationCriteria(Issuer issuer) { private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { Set credentials = new HashSet<>(); - Collection keys = registration.getAssertingPartyDetails() + Collection keys = registration.getAssertingPartyMetadata() .getVerificationX509Credentials(); for (Saml2X509Credential key : keys) { BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + cred.setEntityId(registration.getAssertingPartyMetadata().getEntityId()); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java index b234935b11e..ab51f9bbc5c 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequest.java @@ -190,8 +190,8 @@ public static final class Builder { private Builder(RelyingPartyRegistration registration) { this.registration = registration; - this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceLocation(); - this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + this.location = registration.getAssertingPartyMetadata().getSingleLogoutServiceLocation(); + this.binding = registration.getAssertingPartyMetadata().getSingleLogoutServiceBinding(); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java index a555b784a22..ebc3af89569 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponse.java @@ -156,8 +156,8 @@ public static final class Builder { private Function, String> encoder = DEFAULT_ENCODER; private Builder(RelyingPartyRegistration registration) { - this.location = registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation(); - this.binding = registration.getAssertingPartyDetails().getSingleLogoutServiceBinding(); + this.location = registration.getAssertingPartyMetadata().getSingleLogoutServiceResponseLocation(); + this.binding = registration.getAssertingPartyMetadata().getSingleLogoutServiceBinding(); } /** diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java index 9ad760c3e2b..ae1fcb63fe6 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/metadata/OpenSamlSigningUtils.java @@ -80,7 +80,13 @@ static String serialize(XMLObject object) { } static O sign(O object, RelyingPartyRegistration relyingPartyRegistration) { - SignatureSigningParameters parameters = resolveSigningParameters(relyingPartyRegistration); + List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); + List credentials = resolveSigningCredentials(relyingPartyRegistration); + return sign(object, algorithms, credentials); + } + + static O sign(O object, List algorithms, List credentials) { + SignatureSigningParameters parameters = resolveSigningParameters(algorithms, credentials); try { SignatureSupport.signObject(object, parameters); return object; @@ -97,7 +103,12 @@ static QueryParametersPartial sign(RelyingPartyRegistration registration) { private static SignatureSigningParameters resolveSigningParameters( RelyingPartyRegistration relyingPartyRegistration) { List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); + return resolveSigningParameters(algorithms, credentials); + } + + private static SignatureSigningParameters resolveSigningParameters(List algorithms, + List credentials) { List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); 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 00000000000..c75de010d4e --- /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 00000000000..d03cea8c8cc --- /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/InMemoryRelyingPartyRegistrationRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java index 01f4778027d..7994843c250 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/InMemoryRelyingPartyRegistrationRepository.java @@ -69,7 +69,7 @@ private static Map> createMappingByAssert Collection rps) { MultiValueMap result = new LinkedMultiValueMap<>(); for (RelyingPartyRegistration rp : rps) { - result.add(rp.getAssertingPartyDetails().getEntityId(), rp); + result.add(rp.getAssertingPartyMetadata().getEntityId(), rp); } return Collections.unmodifiableMap(result); } diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepository.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepository.java new file mode 100644 index 00000000000..847883dc0fa --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepository.java @@ -0,0 +1,383 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.annotation.Nonnull; + +import net.shibboleth.utilities.java.support.component.ComponentInitializationException; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.resolver.ResolverException; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.saml.criterion.EntityRoleCriterion; +import org.opensaml.saml.metadata.IterableMetadataSource; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.metadata.resolver.filter.impl.SignatureValidationFilter; +import org.opensaml.saml.metadata.resolver.impl.AbstractBatchMetadataResolver; +import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver; +import org.opensaml.saml.metadata.resolver.index.MetadataIndex; +import org.opensaml.saml.metadata.resolver.index.impl.RoleMetadataIndex; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.security.credential.Credential; +import org.opensaml.security.credential.impl.CollectionCredentialResolver; +import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap; +import org.opensaml.xmlsec.signature.support.SignatureTrustEngine; +import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.util.Assert; + +/** + * An implementation of {@link AssertingPartyMetadataRepository} that uses a + * {@link MetadataResolver} to retrieve {@link AssertingPartyMetadata} instances. + * + *

+ * The {@link MetadataResolver} constructed in {@link #withTrustedMetadataLocation} + * provides expiry-aware refreshing. + * + * @author Josh Cummings + * @since 6.4 + * @see AssertingPartyMetadataRepository + * @see RelyingPartyRegistrations + */ +public final class OpenSamlAssertingPartyMetadataRepository implements AssertingPartyMetadataRepository { + + static { + OpenSamlInitializationService.initialize(); + } + + private final MetadataResolver metadataResolver; + + private final Supplier> descriptors; + + /** + * Construct an {@link OpenSamlAssertingPartyMetadataRepository} using the provided + * {@link MetadataResolver}. + * + *

+ * The {@link MetadataResolver} should either be of type + * {@link IterableMetadataSource} or it should have a {@link RoleMetadataIndex} + * configured. + * @param metadataResolver the {@link MetadataResolver} to use + */ + public OpenSamlAssertingPartyMetadataRepository(MetadataResolver metadataResolver) { + Assert.notNull(metadataResolver, "metadataResolver cannot be null"); + if (isRoleIndexed(metadataResolver)) { + this.descriptors = this::allIndexedEntities; + } + else if (metadataResolver instanceof IterableMetadataSource source) { + this.descriptors = source::iterator; + } + else { + throw new IllegalArgumentException( + "metadataResolver must be an IterableMetadataSource or have a RoleMetadataIndex"); + } + this.metadataResolver = metadataResolver; + } + + private static boolean isRoleIndexed(MetadataResolver resolver) { + if (!(resolver instanceof AbstractBatchMetadataResolver batch)) { + return false; + } + for (MetadataIndex index : batch.getIndexes()) { + if (index instanceof RoleMetadataIndex) { + return true; + } + } + return false; + } + + private Iterator allIndexedEntities() { + CriteriaSet all = new CriteriaSet(new EntityRoleCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME)); + try { + return this.metadataResolver.resolve(all).iterator(); + } + catch (ResolverException ex) { + throw new Saml2Exception(ex); + } + } + + @Override + @NonNull + public Iterator iterator() { + Iterator descriptors = this.descriptors.get(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return descriptors.hasNext(); + } + + @Override + public AssertingPartyMetadata next() { + return OpenSamlAssertingPartyDetails.withEntityDescriptor(descriptors.next()).build(); + } + }; + } + + @Nullable + @Override + public AssertingPartyMetadata findByEntityId(String entityId) { + CriteriaSet byEntityId = new CriteriaSet(new EntityIdCriterion(entityId)); + EntityDescriptor descriptor = resolveSingle(byEntityId); + if (descriptor == null) { + return null; + } + return OpenSamlAssertingPartyDetails.withEntityDescriptor(descriptor).build(); + } + + private EntityDescriptor resolveSingle(CriteriaSet criteria) { + try { + return this.metadataResolver.resolveSingle(criteria); + } + catch (ResolverException ex) { + throw new Saml2Exception(ex); + } + } + + /** + * Use this trusted {@code metadataLocation} to retrieve refreshable, expiry-aware + * SAML 2.0 Asserting Party (IDP) metadata. + * + *

+ * Valid locations can be classpath- or file-based or they can be HTTPS endpoints. + * Some valid endpoints might include: + * + *

+	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
+	 *   metadataLocation = "file:asserting-party-metadata.xml";
+	 *   metadataLocation = "https://ap.example.org/metadata";
+	 * 
+ * + *

+ * Resolution of location is attempted immediately. To defer, wrap in + * {@link CachingRelyingPartyRegistrationRepository}. + * @param metadataLocation the classpath- or file-based locations or HTTPS endpoints + * of the asserting party metadata file + * @return the {@link MetadataLocationRepositoryBuilder} for further configuration + */ + public static MetadataLocationRepositoryBuilder withTrustedMetadataLocation(String metadataLocation) { + return new MetadataLocationRepositoryBuilder(metadataLocation, true); + } + + /** + * Use this {@code metadataLocation} to retrieve refreshable, expiry-aware SAML 2.0 + * Asserting Party (IDP) metadata. Verification credentials are required. + * + *

+ * Valid locations can be classpath- or file-based or they can be remote endpoints. + * Some valid endpoints might include: + * + *

+	 *   metadataLocation = "classpath:asserting-party-metadata.xml";
+	 *   metadataLocation = "file:asserting-party-metadata.xml";
+	 *   metadataLocation = "https://ap.example.org/metadata";
+	 * 
+ * + *

+ * Resolution of location is attempted immediately. To defer, wrap in + * {@link CachingRelyingPartyRegistrationRepository}. + * @param metadataLocation the classpath- or file-based locations or remote endpoints + * of the asserting party metadata file + * @return the {@link MetadataLocationRepositoryBuilder} for further configuration + */ + public static MetadataLocationRepositoryBuilder withMetadataLocation(String metadataLocation) { + return new MetadataLocationRepositoryBuilder(metadataLocation, false); + } + + /** + * A builder class for configuring {@link OpenSamlAssertingPartyMetadataRepository} + * for a specific metadata location. + * + * @author Josh Cummings + */ + public static final class MetadataLocationRepositoryBuilder { + + private final String metadataLocation; + + private final boolean requireVerificationCredentials; + + private final Collection verificationCredentials = new ArrayList<>(); + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + private MetadataLocationRepositoryBuilder(String metadataLocation, boolean trusted) { + this.metadataLocation = metadataLocation; + this.requireVerificationCredentials = !trusted; + } + + /** + * Apply this {@link Consumer} to the list of {@link Saml2X509Credential}s to use + * for verifying metadata signatures. + * + *

+ * If no credentials are supplied, no signature verification is performed. + * @param credentials a {@link Consumer} of the {@link Collection} of + * {@link Saml2X509Credential}s + * @return the {@link MetadataLocationRepositoryBuilder} for further configuration + */ + public MetadataLocationRepositoryBuilder verificationCredentials(Consumer> credentials) { + credentials.accept(this.verificationCredentials); + return this; + } + + /** + * Use this {@link ResourceLoader} for resolving the {@code metadataLocation} + * @param resourceLoader the {@link ResourceLoader} to use + * @return the {@link MetadataLocationRepositoryBuilder} for further configuration + */ + public MetadataLocationRepositoryBuilder resourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + return this; + } + + /** + * Build the {@link OpenSamlAssertingPartyMetadataRepository} + * @return the {@link OpenSamlAssertingPartyMetadataRepository} + */ + public OpenSamlAssertingPartyMetadataRepository build() { + ResourceBackedMetadataResolver metadataResolver = metadataResolver(); + if (!this.verificationCredentials.isEmpty()) { + SignatureTrustEngine engine = new ExplicitKeySignatureTrustEngine( + new CollectionCredentialResolver(this.verificationCredentials), + DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver()); + SignatureValidationFilter filter = new SignatureValidationFilter(engine); + filter.setRequireSignedRoot(true); + metadataResolver.setMetadataFilter(filter); + return new OpenSamlAssertingPartyMetadataRepository(initialize(metadataResolver)); + } + Assert.isTrue(!this.requireVerificationCredentials, "Verification credentials are required"); + return new OpenSamlAssertingPartyMetadataRepository(initialize(metadataResolver)); + } + + private ResourceBackedMetadataResolver metadataResolver() { + Resource resource = this.resourceLoader.getResource(this.metadataLocation); + try { + return new ResourceBackedMetadataResolver(new SpringResource(resource)); + } + catch (IOException ex) { + throw new Saml2Exception(ex); + } + } + + private MetadataResolver initialize(ResourceBackedMetadataResolver metadataResolver) { + try { + metadataResolver.setId(this.getClass().getName() + ".metadataResolver"); + metadataResolver.setParserPool(XMLObjectProviderRegistrySupport.getParserPool()); + metadataResolver.setIndexes(Set.of(new RoleMetadataIndex())); + metadataResolver.initialize(); + return metadataResolver; + } + catch (ComponentInitializationException ex) { + throw new Saml2Exception(ex); + } + } + + private static final class SpringResource implements net.shibboleth.utilities.java.support.resource.Resource { + + private final Resource resource; + + SpringResource(Resource resource) { + this.resource = resource; + } + + @Override + public boolean exists() { + return this.resource.exists(); + } + + @Override + public boolean isReadable() { + return this.resource.isReadable(); + } + + @Override + public boolean isOpen() { + return this.resource.isOpen(); + } + + @Override + public URL getURL() throws IOException { + return this.resource.getURL(); + } + + @Override + public URI getURI() throws IOException { + return this.resource.getURI(); + } + + @Override + public File getFile() throws IOException { + return this.resource.getFile(); + } + + @Nonnull + @Override + public InputStream getInputStream() throws IOException { + return this.resource.getInputStream(); + } + + @Override + public long contentLength() throws IOException { + return this.resource.contentLength(); + } + + @Override + public long lastModified() throws IOException { + return this.resource.lastModified(); + } + + @Override + public net.shibboleth.utilities.java.support.resource.Resource createRelativeResource(String relativePath) + throws IOException { + return new SpringResource(this.resource.createRelative(relativePath)); + } + + @Override + public String getFilename() { + return this.resource.getFilename(); + } + + @Override + public String getDescription() { + return this.resource.getDescription(); + } + + } + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java index 9b3f26dc3a9..8f9d585254f 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/OpenSamlRelyingPartyRegistration.java @@ -36,7 +36,7 @@ * EntityDescriptor descriptor = openSamlRegistration.getAssertingPartyDetails.getEntityDescriptor(); * } * do instead:

- * 	if (registration.getAssertingPartyDetails() instanceof openSamlAssertingPartyDetails) {
+ * 	if (registration.getAssertingPartyMetadata() instanceof openSamlAssertingPartyDetails) {
  * 	    EntityDescriptor descriptor = openSamlAssertingPartyDetails.getEntityDescriptor();
  * 	}
  * 
@@ -170,6 +170,11 @@ public Builder assertingPartyDetails(Consumer ass return (Builder) super.assertingPartyDetails(assertingPartyDetails); } + @Override + public Builder assertingPartyMetadata(Consumer> assertingPartyMetadata) { + return (Builder) super.assertingPartyMetadata(assertingPartyMetadata); + } + /** * Build an {@link OpenSamlRelyingPartyRegistration} * {@link org.springframework.security.saml2.provider.service.registration.OpenSamlRelyingPartyRegistration} 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 87cfea754ec..e18c9499bc0 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 @@ -88,7 +88,7 @@ public class RelyingPartyRegistration { private final boolean authnRequestsSigned; - private final AssertingPartyDetails assertingPartyDetails; + private final AssertingPartyMetadata assertingPartyMetadata; private final Collection decryptionX509Credentials; @@ -127,7 +127,45 @@ protected RelyingPartyRegistration(String registrationId, String entityId, Strin this.singleLogoutServiceBindings = Collections.unmodifiableList(new LinkedList<>(singleLogoutServiceBindings)); this.nameIdFormat = nameIdFormat; this.authnRequestsSigned = authnRequestsSigned; - this.assertingPartyDetails = assertingPartyDetails; + this.assertingPartyMetadata = assertingPartyDetails; + this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); + this.signingX509Credentials = Collections.unmodifiableList(new LinkedList<>(signingX509Credentials)); + } + + private RelyingPartyRegistration(String registrationId, String entityId, String assertionConsumerServiceLocation, + Saml2MessageBinding assertionConsumerServiceBinding, String singleLogoutServiceLocation, + String singleLogoutServiceResponseLocation, Collection singleLogoutServiceBindings, + AssertingPartyMetadata assertingPartyMetadata, String nameIdFormat, boolean authnRequestsSigned, + Collection decryptionX509Credentials, + Collection signingX509Credentials) { + Assert.hasText(registrationId, "registrationId cannot be empty"); + Assert.hasText(entityId, "entityId cannot be empty"); + Assert.hasText(assertionConsumerServiceLocation, "assertionConsumerServiceLocation cannot be empty"); + Assert.notNull(assertionConsumerServiceBinding, "assertionConsumerServiceBinding cannot be null"); + Assert.isTrue(singleLogoutServiceLocation == null || !CollectionUtils.isEmpty(singleLogoutServiceBindings), + "singleLogoutServiceBindings cannot be null or empty when singleLogoutServiceLocation is set"); + Assert.notNull(assertingPartyMetadata, "assertingPartyDetails cannot be null"); + Assert.notNull(decryptionX509Credentials, "decryptionX509Credentials cannot be null"); + for (Saml2X509Credential c : decryptionX509Credentials) { + Assert.notNull(c, "decryptionX509Credentials cannot contain null elements"); + Assert.isTrue(c.isDecryptionCredential(), + "All decryptionX509Credentials must have a usage of DECRYPTION set"); + } + Assert.notNull(signingX509Credentials, "signingX509Credentials cannot be null"); + for (Saml2X509Credential c : signingX509Credentials) { + Assert.notNull(c, "signingX509Credentials cannot contain null elements"); + Assert.isTrue(c.isSigningCredential(), "All signingX509Credentials must have a usage of SIGNING set"); + } + this.registrationId = registrationId; + this.entityId = entityId; + this.assertionConsumerServiceLocation = assertionConsumerServiceLocation; + this.assertionConsumerServiceBinding = assertionConsumerServiceBinding; + this.singleLogoutServiceLocation = singleLogoutServiceLocation; + this.singleLogoutServiceResponseLocation = singleLogoutServiceResponseLocation; + this.singleLogoutServiceBindings = Collections.unmodifiableList(new LinkedList<>(singleLogoutServiceBindings)); + this.nameIdFormat = nameIdFormat; + this.authnRequestsSigned = authnRequestsSigned; + this.assertingPartyMetadata = assertingPartyMetadata; this.decryptionX509Credentials = Collections.unmodifiableList(new LinkedList<>(decryptionX509Credentials)); this.signingX509Credentials = Collections.unmodifiableList(new LinkedList<>(signingX509Credentials)); } @@ -139,7 +177,7 @@ protected RelyingPartyRegistration(String registrationId, String entityId, Strin * @since 6.1 */ public Builder mutate() { - return new Builder(this.registrationId, this.assertingPartyDetails.mutate()).entityId(this.entityId) + return new Builder(this.registrationId, this.assertingPartyMetadata.mutate()).entityId(this.entityId) .signingX509Credentials((c) -> c.addAll(this.signingX509Credentials)) .decryptionX509Credentials((c) -> c.addAll(this.decryptionX509Credentials)) .assertionConsumerServiceLocation(this.assertionConsumerServiceLocation) @@ -317,9 +355,22 @@ public Collection getSigningX509Credentials() { * Get the configuration details for the Asserting Party * @return the {@link AssertingPartyDetails} * @since 5.4 + * @deprecated Use {@link #getAssertingPartyMetadata()} instead */ + @Deprecated public AssertingPartyDetails getAssertingPartyDetails() { - return this.assertingPartyDetails; + Assert.isInstanceOf(AssertingPartyDetails.class, this.assertingPartyMetadata, + "This class was initialized with an AssertingPartyMetadata, please call #getAssertingPartyMetadata instead"); + return (AssertingPartyDetails) this.assertingPartyMetadata; + } + + /** + * Get the metadata for the Asserting Party + * @return the {@link AssertingPartyDetails} + * @since 6.4 + */ + public AssertingPartyMetadata getAssertingPartyMetadata() { + return this.assertingPartyMetadata; } /** @@ -333,11 +384,36 @@ public static Builder withRegistrationId(String registrationId) { return new Builder(registrationId, new AssertingPartyDetails.Builder()); } + /** + * @param assertingPartyDetails the asserting party metadata + * @return {@code Builder} to create a {@code RelyingPartyRegistration} object + * @deprecated Use {@link #withAssertingPartyMetadata} instead + */ + @Deprecated(forRemoval = true, since = "6.4") public static Builder withAssertingPartyDetails(AssertingPartyDetails assertingPartyDetails) { Assert.notNull(assertingPartyDetails, "assertingPartyDetails cannot be null"); 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.notNull(metadata, "assertingPartyMetadata cannot be null"); + return new Builder(metadata.getEntityId(), metadata.mutate()); + } + /** * Creates a {@code RelyingPartyRegistration} {@link Builder} based on an existing * object @@ -380,7 +456,7 @@ public static Builder withRelyingPartyRegistration(RelyingPartyRegistration regi * * @since 5.4 */ - public static class AssertingPartyDetails { + public static class AssertingPartyDetails implements AssertingPartyMetadata { private final String entityId; @@ -584,7 +660,7 @@ public AssertingPartyDetails.Builder mutate() { .singleLogoutServiceBinding(this.singleLogoutServiceBinding); } - public static class Builder { + public static class Builder implements AssertingPartyMetadata.Builder { private String entityId; @@ -800,11 +876,11 @@ public static class Builder { private boolean authnRequestsSigned = false; - private AssertingPartyDetails.Builder assertingPartyDetailsBuilder; + private AssertingPartyMetadata.Builder assertingPartyMetadataBuilder; - protected Builder(String registrationId, AssertingPartyDetails.Builder assertingPartyDetailsBuilder) { + protected Builder(String registrationId, AssertingPartyMetadata.Builder assertingPartyMetadataBuilder) { this.registrationId = registrationId; - this.assertingPartyDetailsBuilder = assertingPartyDetailsBuilder; + this.assertingPartyMetadataBuilder = assertingPartyMetadataBuilder; } /** @@ -1009,9 +1085,24 @@ public Builder authnRequestsSigned(Boolean authnRequestsSigned) { * @param assertingPartyDetails The {@link Consumer} to apply * @return the {@link Builder} for further configuration * @since 5.4 + * @deprecated Use {@link #assertingPartyMetadata} instead */ + @Deprecated(forRemoval = true, since = "6.4") public Builder assertingPartyDetails(Consumer assertingPartyDetails) { - assertingPartyDetails.accept(this.assertingPartyDetailsBuilder); + Assert.isInstanceOf(AssertingPartyDetails.Builder.class, this.assertingPartyMetadataBuilder, + "This class was constructed with an AssertingPartyMetadata instance, as such, please use #assertingPartyMetadata"); + assertingPartyDetails.accept((AssertingPartyDetails.Builder) this.assertingPartyMetadataBuilder); + return this; + } + + /** + * Apply this {@link Consumer} to further configure the Asserting Party metadata + * @param assertingPartyMetadata The {@link Consumer} to apply + * @return the {@link Builder} for further configuration + * @since 6.4 + */ + public Builder assertingPartyMetadata(Consumer> assertingPartyMetadata) { + assertingPartyMetadata.accept(this.assertingPartyMetadataBuilder); return this; } @@ -1029,7 +1120,7 @@ public RelyingPartyRegistration build() { this.singleLogoutServiceBindings.add(Saml2MessageBinding.POST); } - AssertingPartyDetails party = this.assertingPartyDetailsBuilder.build(); + AssertingPartyMetadata party = this.assertingPartyMetadataBuilder.build(); return new RelyingPartyRegistration(this.registrationId, this.entityId, this.assertionConsumerServiceLocation, this.assertionConsumerServiceBinding, this.singleLogoutServiceLocation, this.singleLogoutServiceResponseLocation, diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java index 1161a029b7d..4a39f151cd5 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/RelyingPartyRegistrationPlaceholderResolvers.java @@ -68,7 +68,7 @@ public static UriResolver uriResolver(HttpServletRequest request) { */ public static UriResolver uriResolver(HttpServletRequest request, RelyingPartyRegistration registration) { String relyingPartyEntityId = registration.getEntityId(); - String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); + String assertingPartyEntityId = registration.getAssertingPartyMetadata().getEntityId(); String registrationId = registration.getRegistrationId(); Map uriVariables = uriVariables(request); uriVariables.put("relyingPartyEntityId", StringUtils.hasText(relyingPartyEntityId) ? relyingPartyEntityId : ""); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java index 85ed7ae877a..2ec777a70e1 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlAuthenticationRequestResolver.java @@ -146,7 +146,7 @@ T resolve(HttpServletRequest requ Issuer iss = this.issuerBuilder.buildObject(); iss.setValue(entityId); authnRequest.setIssuer(iss); - authnRequest.setDestination(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + authnRequest.setDestination(registration.getAssertingPartyMetadata().getSingleSignOnServiceLocation()); authnRequest.setAssertionConsumerServiceURL(assertionConsumerServiceLocation); if (registration.getNameIdFormat() != null) { NameIDPolicy nameIdPolicy = this.nameIdPolicyBuilder.buildObject(); @@ -158,9 +158,9 @@ T resolve(HttpServletRequest requ authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1)); } String relayState = this.relayStateResolver.convert(request); - Saml2MessageBinding binding = registration.getAssertingPartyDetails().getSingleSignOnServiceBinding(); + Saml2MessageBinding binding = registration.getAssertingPartyMetadata().getSingleSignOnServiceBinding(); if (binding == Saml2MessageBinding.POST) { - if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned() + if (registration.getAssertingPartyMetadata().getWantAuthnRequestsSigned() || registration.isAuthnRequestsSigned()) { OpenSamlSigningUtils.sign(authnRequest, registration); } @@ -180,7 +180,7 @@ T resolve(HttpServletRequest requ .samlRequest(deflatedAndEncoded) .relayState(relayState) .id(authnRequest.getID()); - if (registration.getAssertingPartyDetails().getWantAuthnRequestsSigned() + if (registration.getAssertingPartyMetadata().getWantAuthnRequestsSigned() || registration.isAuthnRequestsSigned()) { OpenSamlSigningUtils.QueryParametersPartial parametersPartial = OpenSamlSigningUtils.sign(registration) .param(Saml2ParameterNames.SAML_REQUEST, deflatedAndEncoded); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java index df9d861065f..c479b06cf53 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlSigningUtils.java @@ -95,7 +95,7 @@ static QueryParametersPartial sign(RelyingPartyRegistration registration) { private static SignatureSigningParameters resolveSigningParameters( RelyingPartyRegistration relyingPartyRegistration) { List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java index 137aff26d85..7d52867e4d9 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/OpenSamlVerificationUtils.java @@ -155,12 +155,12 @@ private CriteriaSet verificationCriteria(Issuer issuer) { private SignatureTrustEngine trustEngine(RelyingPartyRegistration registration) { Set credentials = new HashSet<>(); - Collection keys = registration.getAssertingPartyDetails() + Collection keys = registration.getAssertingPartyMetadata() .getVerificationX509Credentials(); for (Saml2X509Credential key : keys) { BasicX509Credential cred = new BasicX509Credential(key.getCertificate()); cred.setUsageType(UsageType.SIGNING); - cred.setEntityId(registration.getAssertingPartyDetails().getEntityId()); + cred.setEntityId(registration.getAssertingPartyMetadata().getEntityId()); credentials.add(cred); } CredentialResolver credentialsResolver = new CollectionCredentialResolver(credentials); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java index 69d9ccc4a11..937233146ec 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutRequestResolver.java @@ -126,13 +126,13 @@ Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentica if (registration == null) { return null; } - if (registration.getAssertingPartyDetails().getSingleLogoutServiceLocation() == null) { + if (registration.getAssertingPartyMetadata().getSingleLogoutServiceLocation() == null) { return null; } UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); String entityId = uriResolver.resolve(registration.getEntityId()); LogoutRequest logoutRequest = this.logoutRequestBuilder.buildObject(); - logoutRequest.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceLocation()); + logoutRequest.setDestination(registration.getAssertingPartyMetadata().getSingleLogoutServiceLocation()); Issuer issuer = this.issuerBuilder.buildObject(); issuer.setValue(entityId); logoutRequest.setIssuer(issuer); @@ -154,7 +154,7 @@ Saml2LogoutRequest resolve(HttpServletRequest request, Authentication authentica String relayState = this.relayStateResolver.convert(request); Saml2LogoutRequest.Builder result = Saml2LogoutRequest.withRelyingPartyRegistration(registration) .id(logoutRequest.getID()); - if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { + if (registration.getAssertingPartyMetadata().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { String xml = serialize(OpenSamlSigningUtils.sign(logoutRequest, registration)); String samlRequest = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)); return result.samlRequest(samlRequest).relayState(relayState).build(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java index b726eb6d328..531bcad6026 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlLogoutResponseResolver.java @@ -143,13 +143,14 @@ Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentic if (registration == null) { return null; } - if (registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation() == null) { + if (registration.getAssertingPartyMetadata().getSingleLogoutServiceResponseLocation() == null) { return null; } UriResolver uriResolver = RelyingPartyRegistrationPlaceholderResolvers.uriResolver(request, registration); String entityId = uriResolver.resolve(registration.getEntityId()); LogoutResponse logoutResponse = this.logoutResponseBuilder.buildObject(); - logoutResponse.setDestination(registration.getAssertingPartyDetails().getSingleLogoutServiceResponseLocation()); + logoutResponse + .setDestination(registration.getAssertingPartyMetadata().getSingleLogoutServiceResponseLocation()); Issuer issuer = this.issuerBuilder.buildObject(); issuer.setValue(entityId); logoutResponse.setIssuer(issuer); @@ -164,7 +165,7 @@ Saml2LogoutResponse resolve(HttpServletRequest request, Authentication authentic } logoutResponseConsumer.accept(registration, logoutResponse); Saml2LogoutResponse.Builder result = Saml2LogoutResponse.withRelyingPartyRegistration(registration); - if (registration.getAssertingPartyDetails().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { + if (registration.getAssertingPartyMetadata().getSingleLogoutServiceBinding() == Saml2MessageBinding.POST) { String xml = serialize(OpenSamlSigningUtils.sign(logoutResponse, registration)); String samlResponse = Saml2Utils.samlEncode(xml.getBytes(StandardCharsets.UTF_8)); result.samlResponse(samlResponse); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtils.java index f30c00cf0e1..0b7ef324ddb 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtils.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/OpenSamlSigningUtils.java @@ -96,7 +96,7 @@ static QueryParametersPartial sign(RelyingPartyRegistration registration) { private static SignatureSigningParameters resolveSigningParameters( RelyingPartyRegistration relyingPartyRegistration) { List credentials = resolveSigningCredentials(relyingPartyRegistration); - List algorithms = relyingPartyRegistration.getAssertingPartyDetails().getSigningAlgorithms(); + List algorithms = relyingPartyRegistration.getAssertingPartyMetadata().getSigningAlgorithms(); List digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256); String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS; SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver(); diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java index c0a8d850fa1..49f7b3e7cdb 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.java @@ -28,7 +28,7 @@ * * The returned logout request is suitable for sending to the asserting party based on, * for example, the location and binding specified in - * {@link RelyingPartyRegistration#getAssertingPartyDetails()}. + * {@link RelyingPartyRegistration#getAssertingPartyMetadata()}. * * @author Josh Cummings * @since 5.6 diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java index 84cf038af9b..f722fd55b77 100644 --- a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.java @@ -28,7 +28,7 @@ * * The returned logout response is suitable for sending to the asserting party based on, * for example, the location and binding specified in - * {@link RelyingPartyRegistration#getAssertingPartyDetails()}. + * {@link RelyingPartyRegistration#getAssertingPartyMetadata()}. * * @author Josh Cummings * @since 5.6 diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java index abe4a4549a0..0193a531470 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml4AuthenticationProviderTests.java @@ -51,7 +51,6 @@ import org.opensaml.saml.saml2.core.EncryptedAssertion; import org.opensaml.saml.saml2.core.EncryptedAttribute; import org.opensaml.saml.saml2.core.EncryptedID; -import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.NameID; import org.opensaml.saml.saml2.core.OneTimeUse; import org.opensaml.saml.saml2.core.ProxyRestriction; @@ -737,16 +736,7 @@ public void authenticateWhenCustomResponseValidatorThenUses() { @Test public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatusCodes() { - ResponseToken mockResponseToken = mock(ResponseToken.class); - Saml2AuthenticationToken mockSamlToken = mock(Saml2AuthenticationToken.class); - given(mockResponseToken.getToken()).willReturn(mockSamlToken); - - RelyingPartyRegistration mockRelyingPartyRegistration = mock(RelyingPartyRegistration.class); - given(mockSamlToken.getRelyingPartyRegistration()).willReturn(mockRelyingPartyRegistration); - - RelyingPartyRegistration.AssertingPartyDetails mockAssertingPartyDetails = mock( - RelyingPartyRegistration.AssertingPartyDetails.class); - given(mockRelyingPartyRegistration.getAssertingPartyDetails()).willReturn(mockAssertingPartyDetails); + Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token(); Status parentStatus = new StatusBuilder().buildObject(); StatusCode parentStatusCode = new StatusCodeBuilder().buildObject(); @@ -756,40 +746,27 @@ public void authenticateWhenResponseStatusIsNotSuccessThenOnlyReturnParentStatus parentStatusCode.setStatusCode(childStatusCode); parentStatus.setStatusCode(parentStatusCode); - Response mockResponse = mock(Response.class); - given(mockResponse.getStatus()).willReturn(parentStatus); - Issuer mockIssuer = mock(Issuer.class); - given(mockIssuer.getValue()).willReturn("mockedIssuer"); - given(mockResponse.getIssuer()).willReturn(mockIssuer); - - given(mockResponseToken.getResponse()).willReturn(mockResponse); + Response response = TestOpenSamlObjects.response(); + response.setStatus(parentStatus); + response.setIssuer(TestOpenSamlObjects.issuer("mockedIssuer")); Converter validator = OpenSaml4AuthenticationProvider .createDefaultResponseValidator(); - Saml2ResponseValidatorResult result = validator.convert(mockResponseToken); + Saml2ResponseValidatorResult result = validator.convert(new ResponseToken(response, token)); String expectedErrorMessage = String.format("Invalid status [%s] for SAML response", parentStatusCode.getValue()); assertThat( - result.getErrors().stream().anyMatch((error) -> error.getDescription().contains(expectedErrorMessage))); + result.getErrors().stream().anyMatch((error) -> error.getDescription().contains(expectedErrorMessage))) + .isTrue(); assertThat(result.getErrors() .stream() - .noneMatch((error) -> error.getDescription().contains(childStatusCode.getValue()))); + .noneMatch((error) -> error.getDescription().contains(childStatusCode.getValue()))).isTrue(); } @Test public void authenticateWhenResponseStatusIsNotSuccessThenReturnParentAndChildStatusCode() { - ResponseToken mockResponseToken = mock(ResponseToken.class); - Saml2AuthenticationToken mockSamlToken = mock(Saml2AuthenticationToken.class); - given(mockResponseToken.getToken()).willReturn(mockSamlToken); - - RelyingPartyRegistration mockRelyingPartyRegistration = mock(RelyingPartyRegistration.class); - given(mockSamlToken.getRelyingPartyRegistration()).willReturn(mockRelyingPartyRegistration); - - RelyingPartyRegistration.AssertingPartyDetails mockAssertingPartyDetails = mock( - RelyingPartyRegistration.AssertingPartyDetails.class); - given(mockRelyingPartyRegistration.getAssertingPartyDetails()).willReturn(mockAssertingPartyDetails); - + Saml2AuthenticationToken token = TestSaml2AuthenticationTokens.token(); Status parentStatus = new StatusBuilder().buildObject(); StatusCode parentStatusCode = new StatusCodeBuilder().buildObject(); parentStatusCode.setValue(StatusCode.REQUESTER); @@ -798,17 +775,13 @@ public void authenticateWhenResponseStatusIsNotSuccessThenReturnParentAndChildSt parentStatusCode.setStatusCode(childStatusCode); parentStatus.setStatusCode(parentStatusCode); - Response mockResponse = mock(Response.class); - given(mockResponse.getStatus()).willReturn(parentStatus); - Issuer mockIssuer = mock(Issuer.class); - given(mockIssuer.getValue()).willReturn("mockedIssuer"); - given(mockResponse.getIssuer()).willReturn(mockIssuer); - - given(mockResponseToken.getResponse()).willReturn(mockResponse); + Response response = TestOpenSamlObjects.response(); + response.setStatus(parentStatus); + response.setIssuer(TestOpenSamlObjects.issuer("mockedIssuer")); Converter validator = OpenSaml4AuthenticationProvider .createDefaultResponseValidator(); - Saml2ResponseValidatorResult result = validator.convert(mockResponseToken); + Saml2ResponseValidatorResult result = validator.convert(new ResponseToken(response, token)); String expectedParentErrorMessage = String.format("Invalid status [%s] for SAML response", parentStatusCode.getValue()); @@ -816,10 +789,10 @@ public void authenticateWhenResponseStatusIsNotSuccessThenReturnParentAndChildSt childStatusCode.getValue()); assertThat(result.getErrors() .stream() - .anyMatch((error) -> error.getDescription().contains(expectedParentErrorMessage))); + .anyMatch((error) -> error.getDescription().contains(expectedParentErrorMessage))).isTrue(); assertThat(result.getErrors() .stream() - .anyMatch((error) -> error.getDescription().contains(expectedChildErrorMessage))); + .anyMatch((error) -> error.getDescription().contains(expectedChildErrorMessage))).isTrue(); } @Test diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java index 0c5657e2005..7ecdaeb20b8 100644 --- a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/authentication/TestOpenSamlObjects.java @@ -45,6 +45,7 @@ import org.opensaml.core.xml.schema.impl.XSURIBuilder; import org.opensaml.saml.common.SAMLVersion; import org.opensaml.saml.common.SignableSAMLObject; +import org.opensaml.saml.common.xml.SAMLConstants; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeStatement; @@ -74,6 +75,14 @@ import org.opensaml.saml.saml2.core.impl.StatusBuilder; import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder; import org.opensaml.saml.saml2.encryption.Encrypter; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.saml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml.saml2.metadata.SingleSignOnService; +import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorBuilder; +import org.opensaml.saml.saml2.metadata.impl.IDPSSODescriptorBuilder; +import org.opensaml.saml.saml2.metadata.impl.KeyDescriptorBuilder; +import org.opensaml.saml.saml2.metadata.impl.SingleSignOnServiceBuilder; import org.opensaml.security.SecurityException; import org.opensaml.security.credential.BasicCredential; import org.opensaml.security.credential.Credential; @@ -83,6 +92,9 @@ import org.opensaml.xmlsec.encryption.support.DataEncryptionParameters; import org.opensaml.xmlsec.encryption.support.EncryptionException; import org.opensaml.xmlsec.encryption.support.KeyEncryptionParameters; +import org.opensaml.xmlsec.keyinfo.KeyInfoSupport; +import org.opensaml.xmlsec.signature.KeyInfo; +import org.opensaml.xmlsec.signature.impl.KeyInfoBuilder; import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignatureSupport; @@ -92,6 +104,7 @@ import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.TestSaml2X509Credentials; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; public final class TestOpenSamlObjects { @@ -221,7 +234,7 @@ public static LogoutRequest logoutRequest() { return logoutRequest; } - static Credential getSigningCredential(Saml2X509Credential credential, String entityId) { + public static Credential getSigningCredential(Saml2X509Credential credential, String entityId) { BasicCredential cred = getBasicCredential(credential); cred.setEntityId(entityId); cred.setUsageType(UsageType.SIGNING); @@ -466,6 +479,39 @@ public static LogoutRequest relyingPartyLogoutRequest(RelyingPartyRegistration r return logoutRequest; } + public static EntityDescriptor entityDescriptor(RelyingPartyRegistration registration) { + EntityDescriptorBuilder entityDescriptorBuilder = new EntityDescriptorBuilder(); + EntityDescriptor entityDescriptor = entityDescriptorBuilder.buildObject(); + entityDescriptor.setEntityID(registration.getAssertingPartyDetails().getEntityId()); + IDPSSODescriptorBuilder idpssoDescriptorBuilder = new IDPSSODescriptorBuilder(); + IDPSSODescriptor idpssoDescriptor = idpssoDescriptorBuilder.buildObject(); + idpssoDescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + SingleSignOnServiceBuilder singleSignOnServiceBuilder = new SingleSignOnServiceBuilder(); + SingleSignOnService singleSignOnService = singleSignOnServiceBuilder.buildObject(); + singleSignOnService.setBinding(Saml2MessageBinding.POST.getUrn()); + singleSignOnService.setLocation(registration.getAssertingPartyDetails().getSingleSignOnServiceLocation()); + idpssoDescriptor.getSingleSignOnServices().add(singleSignOnService); + KeyDescriptorBuilder keyDescriptorBuilder = new KeyDescriptorBuilder(); + KeyDescriptor keyDescriptor = keyDescriptorBuilder.buildObject(); + keyDescriptor.setUse(UsageType.SIGNING); + KeyInfoBuilder keyInfoBuilder = new KeyInfoBuilder(); + KeyInfo keyInfo = keyInfoBuilder.buildObject(); + addCertificate(keyInfo, registration.getSigningX509Credentials().iterator().next().getCertificate()); + keyDescriptor.setKeyInfo(keyInfo); + idpssoDescriptor.getKeyDescriptors().add(keyDescriptor); + entityDescriptor.getRoleDescriptors(IDPSSODescriptor.DEFAULT_ELEMENT_NAME).add(idpssoDescriptor); + return entityDescriptor; + } + + static void addCertificate(KeyInfo info, X509Certificate certificate) { + try { + KeyInfoSupport.addCertificate(info, certificate); + } + catch (Exception ex) { + throw new Saml2Exception(ex); + } + } + static T build(QName qName) { return (T) XMLObjectProviderRegistrySupport.getBuilderFactory().getBuilder(qName).buildObject(qName); } diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepositoryTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepositoryTests.java new file mode 100644 index 00000000000..5c57594bc43 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/registration/OpenSamlAssertingPartyMetadataRepositoryTests.java @@ -0,0 +1,377 @@ +/* + * 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.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import net.shibboleth.utilities.java.support.xml.SerializeSupport; +import okhttp3.mockwebserver.Dispatcher; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; +import org.opensaml.core.xml.io.Marshaller; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.metadata.IterableMetadataSource; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver; +import org.opensaml.saml.metadata.resolver.index.impl.RoleMetadataIndex; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.security.credential.Credential; +import org.w3c.dom.Element; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.OpenSamlInitializationService; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link OpenSamlAssertingPartyMetadataRepository} + */ +public class OpenSamlAssertingPartyMetadataRepositoryTests { + + static { + OpenSamlInitializationService.initialize(); + } + + private String metadata; + + private String entitiesDescriptor; + + @BeforeEach + public void setup() throws Exception { + ClassPathResource resource = new ClassPathResource("test-metadata.xml"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + this.metadata = reader.lines().collect(Collectors.joining()); + } + resource = new ClassPathResource("test-entitiesdescriptor.xml"); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()))) { + this.entitiesDescriptor = reader.lines().collect(Collectors.joining()); + } + } + + @Test + public void withMetadataUrlLocationWhenResolvableThenFindByEntityIdReturns() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.setDispatcher(new AlwaysDispatch(new MockResponse().setBody(this.metadata).setResponseCode(200))); + AssertingPartyMetadataRepository parties = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation(server.url("/").toString()) + .build(); + AssertingPartyMetadata party = parties.findByEntityId("https://idp.example.com/idp/shibboleth"); + assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(party.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(party.getVerificationX509Credentials()).hasSize(1); + assertThat(party.getEncryptionX509Credentials()).hasSize(1); + } + } + + @Test + public void withMetadataUrlLocationnWhenResolvableThenIteratorReturns() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.setDispatcher( + new AlwaysDispatch(new MockResponse().setBody(this.entitiesDescriptor).setResponseCode(200))); + List parties = new ArrayList<>(); + OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation(server.url("/").toString()) + .build() + .iterator() + .forEachRemaining(parties::add); + assertThat(parties).hasSize(2); + assertThat(parties).extracting(AssertingPartyMetadata::getEntityId) + .contains("https://ap.example.org/idp/shibboleth", "https://idp.example.com/idp/shibboleth"); + } + } + + @Test + public void withMetadataUrlLocationWhenUnresolvableThenThrowsSaml2Exception() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue(new MockResponse().setBody(this.metadata).setResponseCode(200)); + String url = server.url("/").toString(); + server.shutdown(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation(url).build()); + } + } + + @Test + public void withMetadataUrlLocationWhenMalformedResponseThenSaml2Exception() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.setDispatcher(new AlwaysDispatch("malformed")); + String url = server.url("/").toString(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation(url).build()); + } + } + + @Test + public void fromMetadataFileLocationWhenResolvableThenFindByEntityIdReturns() { + File file = new File("src/test/resources/test-metadata.xml"); + AssertingPartyMetadata party = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation("file:" + file.getAbsolutePath()) + .build() + .findByEntityId("https://idp.example.com/idp/shibboleth"); + assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(party.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(party.getVerificationX509Credentials()).hasSize(1); + assertThat(party.getEncryptionX509Credentials()).hasSize(1); + } + + @Test + public void fromMetadataFileLocationWhenResolvableThenIteratorReturns() { + File file = new File("src/test/resources/test-entitiesdescriptor.xml"); + Collection parties = new ArrayList<>(); + OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation("file:" + file.getAbsolutePath()) + .build() + .iterator() + .forEachRemaining(parties::add); + assertThat(parties).hasSize(2); + assertThat(parties).extracting(AssertingPartyMetadata::getEntityId) + .contains("https://idp.example.com/idp/shibboleth", "https://ap.example.org/idp/shibboleth"); + } + + @Test + public void withMetadataFileLocationWhenNotFoundThenSaml2Exception() { + assertThatExceptionOfType(Saml2Exception.class).isThrownBy( + () -> OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation("file:path").build()); + } + + @Test + public void fromMetadataClasspathLocationWhenResolvableThenFindByEntityIdReturns() { + AssertingPartyMetadata party = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation("classpath:test-entitiesdescriptor.xml") + .build() + .findByEntityId("https://ap.example.org/idp/shibboleth"); + assertThat(party.getEntityId()).isEqualTo("https://ap.example.org/idp/shibboleth"); + assertThat(party.getSingleSignOnServiceLocation()) + .isEqualTo("https://ap.example.org/idp/profile/SAML2/POST/SSO"); + assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(party.getVerificationX509Credentials()).hasSize(1); + assertThat(party.getEncryptionX509Credentials()).hasSize(1); + } + + @Test + public void fromMetadataClasspathLocationWhenResolvableThenIteratorReturns() { + Collection parties = new ArrayList<>(); + OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation("classpath:test-entitiesdescriptor.xml") + .build() + .iterator() + .forEachRemaining(parties::add); + assertThat(parties).hasSize(2); + assertThat(parties).extracting(AssertingPartyMetadata::getEntityId) + .contains("https://idp.example.com/idp/shibboleth", "https://ap.example.org/idp/shibboleth"); + } + + @Test + public void withMetadataClasspathLocationWhenNotFoundThenSaml2Exception() { + assertThatExceptionOfType(Saml2Exception.class).isThrownBy( + () -> OpenSamlAssertingPartyMetadataRepository.withTrustedMetadataLocation("classpath:path").build()); + } + + @Test + public void withTrustedMetadataLocationWhenMatchingCredentialsThenVerifiesSignature() throws IOException { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration); + TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(), + descriptor.getEntityID()); + String serialized = serialize(descriptor); + Credential credential = TestOpenSamlObjects + .getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID()); + try (MockWebServer server = new MockWebServer()) { + server.start(); + server.setDispatcher(new AlwaysDispatch(serialized)); + AssertingPartyMetadataRepository parties = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation(server.url("/").toString()) + .verificationCredentials((c) -> c.add(credential)) + .build(); + assertThat(parties.findByEntityId(registration.getAssertingPartyDetails().getEntityId())).isNotNull(); + } + } + + @Test + public void withTrustedMetadataLocationWhenMismatchingCredentialsThenSaml2Exception() throws IOException { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration); + TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.relyingPartySigningCredential(), + descriptor.getEntityID()); + String serialized = serialize(descriptor); + Credential credential = TestOpenSamlObjects + .getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID()); + try (MockWebServer server = new MockWebServer()) { + server.start(); + server.setDispatcher(new AlwaysDispatch(serialized)); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation(server.url("/").toString()) + .verificationCredentials((c) -> c.add(credential)) + .build()); + } + } + + @Test + public void withTrustedMetadataLocationWhenNoCredentialsThenSkipsVerifySignature() throws IOException { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration); + TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(), + descriptor.getEntityID()); + String serialized = serialize(descriptor); + try (MockWebServer server = new MockWebServer()) { + server.start(); + server.setDispatcher(new AlwaysDispatch(serialized)); + AssertingPartyMetadataRepository parties = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation(server.url("/").toString()) + .build(); + assertThat(parties.findByEntityId(registration.getAssertingPartyDetails().getEntityId())).isNotNull(); + } + } + + @Test + public void withTrustedMetadataLocationWhenCustomResourceLoaderThenUses() { + ResourceLoader resourceLoader = mock(ResourceLoader.class); + given(resourceLoader.getResource(any())).willReturn(new ClassPathResource("test-metadata.xml")); + AssertingPartyMetadata party = OpenSamlAssertingPartyMetadataRepository + .withTrustedMetadataLocation("classpath:wrong") + .resourceLoader(resourceLoader) + .build() + .iterator() + .next(); + assertThat(party.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth"); + assertThat(party.getSingleSignOnServiceLocation()) + .isEqualTo("https://idp.example.com/idp/profile/SAML2/POST/SSO"); + assertThat(party.getSingleSignOnServiceBinding()).isEqualTo(Saml2MessageBinding.POST); + assertThat(party.getVerificationX509Credentials()).hasSize(1); + assertThat(party.getEncryptionX509Credentials()).hasSize(1); + verify(resourceLoader).getResource(any()); + } + + @Test + public void constructorWhenNoIndexAndNoIteratorThenException() { + MetadataResolver resolver = mock(MetadataResolver.class); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> new OpenSamlAssertingPartyMetadataRepository(resolver)); + } + + @Test + public void constructorWhenIterableResolverThenUses() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration); + MetadataResolver resolver = mock(MetadataResolver.class, + withSettings().extraInterfaces(IterableMetadataSource.class)); + given(((IterableMetadataSource) resolver).iterator()).willReturn(List.of(descriptor).iterator()); + AssertingPartyMetadataRepository parties = new OpenSamlAssertingPartyMetadataRepository(resolver); + parties.iterator() + .forEachRemaining((p) -> assertThat(p.getEntityId()) + .isEqualTo(registration.getAssertingPartyDetails().getEntityId())); + verify(((IterableMetadataSource) resolver)).iterator(); + } + + @Test + public void constructorWhenIndexedResolverThenUses() throws Exception { + FilesystemMetadataResolver resolver = new FilesystemMetadataResolver( + new ClassPathResource("test-metadata.xml").getFile()); + resolver.setIndexes(Set.of(new RoleMetadataIndex())); + resolver.setId("id"); + resolver.setParserPool(XMLObjectProviderRegistrySupport.getParserPool()); + resolver.initialize(); + MetadataResolver spied = spy(resolver); + AssertingPartyMetadataRepository parties = new OpenSamlAssertingPartyMetadataRepository(spied); + parties.iterator() + .forEachRemaining((p) -> assertThat(p.getEntityId()).isEqualTo("https://idp.example.com/idp/shibboleth")); + verify(spied).resolve(any()); + } + + @Test + public void withMetadataLocationWhenNoCredentialsThenException() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> OpenSamlAssertingPartyMetadataRepository.withMetadataLocation("classpath:test-metadata.xml") + .build()); + } + + @Test + public void withMetadataLocationWhenMatchingCredentialsThenVerifiesSignature() throws IOException { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.full().build(); + EntityDescriptor descriptor = TestOpenSamlObjects.entityDescriptor(registration); + TestOpenSamlObjects.signed(descriptor, TestSaml2X509Credentials.assertingPartySigningCredential(), + descriptor.getEntityID()); + String serialized = serialize(descriptor); + Credential credential = TestOpenSamlObjects + .getSigningCredential(TestSaml2X509Credentials.relyingPartyVerifyingCredential(), descriptor.getEntityID()); + try (MockWebServer server = new MockWebServer()) { + server.start(); + server.setDispatcher(new AlwaysDispatch(serialized)); + AssertingPartyMetadataRepository parties = OpenSamlAssertingPartyMetadataRepository + .withMetadataLocation(server.url("/").toString()) + .verificationCredentials((c) -> c.add(credential)) + .build(); + assertThat(parties.findByEntityId(registration.getAssertingPartyDetails().getEntityId())).isNotNull(); + } + } + + private static String serialize(XMLObject object) { + try { + Marshaller marshaller = XMLObjectProviderRegistrySupport.getMarshallerFactory().getMarshaller(object); + Element element = marshaller.marshall(object); + return SerializeSupport.nodeToString(element); + } + catch (MarshallingException ex) { + throw new Saml2Exception(ex); + } + } + + private static final class AlwaysDispatch extends Dispatcher { + + private final MockResponse response; + + private AlwaysDispatch(String body) { + this.response = new MockResponse().setBody(body).setResponseCode(200); + } + + private AlwaysDispatch(MockResponse response) { + this.response = response; + } + + @Override + public MockResponse dispatch(RecordedRequest recordedRequest) throws InterruptedException { + return this.response; + } + + } + +} 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 f85320e932e..b5512e10c9c 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 @@ -16,13 +16,19 @@ package org.springframework.security.saml2.provider.service.registration; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + import org.junit.jupiter.api.Test; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration.AssertingPartyDetails; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class RelyingPartyRegistrationTests { @@ -166,4 +172,186 @@ public void buildPreservesCredentialsOrder() { .containsExactly(encryptingCredential, altApCredential); } + @Test + void withAssertingPartyMetadataWhenMetadataThenBuilderCopies() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .nameIdFormat("format") + .assertingPartyMetadata((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) + .assertingPartyMetadata((a) -> a.wantAuthnRequestsSigned(false)) + .assertingPartyMetadata((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + RelyingPartyRegistration copied = RelyingPartyRegistration + .withAssertingPartyMetadata(registration.getAssertingPartyMetadata()) + .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); + } + + @Test + void withAssertingPartyMetadataWhenMetadataThenDisallowsDetails() { + AssertingPartyMetadata metadata = new CustomAssertingPartyMetadata(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> RelyingPartyRegistration.withAssertingPartyMetadata(metadata) + .assertingPartyDetails((a) -> a.entityId("entity-id")) + .build()); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> RelyingPartyRegistration.withAssertingPartyMetadata(metadata).build().getAssertingPartyDetails()); + } + + @Test + void withAssertingPartyMetadataWhenDetailsThenBuilderCopies() { + RelyingPartyRegistration registration = TestRelyingPartyRegistrations.relyingPartyRegistration() + .nameIdFormat("format") + .assertingPartyMetadata((a) -> a.singleSignOnServiceBinding(Saml2MessageBinding.POST)) + .assertingPartyMetadata((a) -> a.wantAuthnRequestsSigned(false)) + .assertingPartyMetadata((a) -> a.signingAlgorithms((algs) -> algs.add("alg"))) + .assertionConsumerServiceBinding(Saml2MessageBinding.REDIRECT) + .build(); + AssertingPartyDetails details = registration.getAssertingPartyDetails(); + RelyingPartyRegistration copied = RelyingPartyRegistration.withAssertingPartyDetails(details) + .assertingPartyDetails((a) -> a.entityId(details.getEntityId())) + .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); + } + + private static class CustomAssertingPartyMetadata implements AssertingPartyMetadata { + + @Override + public String getEntityId() { + return ""; + } + + @Override + public boolean getWantAuthnRequestsSigned() { + return false; + } + + @Override + public List getSigningAlgorithms() { + return List.of(); + } + + @Override + public Collection getVerificationX509Credentials() { + return List.of(); + } + + @Override + public Collection getEncryptionX509Credentials() { + return List.of(); + } + + @Override + public String getSingleSignOnServiceLocation() { + return ""; + } + + @Override + public Saml2MessageBinding getSingleSignOnServiceBinding() { + return null; + } + + @Override + public String getSingleLogoutServiceLocation() { + return ""; + } + + @Override + public String getSingleLogoutServiceResponseLocation() { + return ""; + } + + @Override + public Saml2MessageBinding getSingleLogoutServiceBinding() { + return null; + } + + @Override + public Builder mutate() { + return new Builder(); + } + + private static class Builder implements AssertingPartyMetadata.Builder { + + @Override + public Builder entityId(String entityId) { + return this; + } + + @Override + public Builder wantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) { + return this; + } + + @Override + public Builder signingAlgorithms(Consumer> signingMethodAlgorithmsConsumer) { + return this; + } + + @Override + public Builder verificationX509Credentials(Consumer> credentialsConsumer) { + return this; + } + + @Override + public Builder encryptionX509Credentials(Consumer> credentialsConsumer) { + return this; + } + + @Override + public Builder singleSignOnServiceLocation(String singleSignOnServiceLocation) { + return this; + } + + @Override + public Builder singleSignOnServiceBinding(Saml2MessageBinding singleSignOnServiceBinding) { + return this; + } + + @Override + public Builder singleLogoutServiceLocation(String singleLogoutServiceLocation) { + return this; + } + + @Override + public Builder singleLogoutServiceResponseLocation(String singleLogoutServiceResponseLocation) { + return this; + } + + @Override + public Builder singleLogoutServiceBinding(Saml2MessageBinding singleLogoutServiceBinding) { + return this; + } + + @Override + public AssertingPartyMetadata build() { + return new CustomAssertingPartyMetadata(); + } + + } + + } + }