From 1c885cf3a369a44b5d4e9006b648e4a93f33ac92 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 28 Feb 2023 12:35:04 -0700 Subject: [PATCH 1/2] Document Federation Usecase Closes gh-12764 --- .../servlet/saml2/login/authentication.adoc | 90 ++++++- .../pages/servlet/saml2/login/overview.adoc | 232 ++++++++++++++---- .../ROOT/pages/servlet/saml2/metadata.adoc | 42 ++++ 3 files changed, 309 insertions(+), 55 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index d4adad3bc6..eb5ff67778 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -1,17 +1,97 @@ [[servlet-saml2login-authenticate-responses]] = Authenticating ````s -To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] by default. +To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it. You can configure this in a number of ways including: -1. Setting a clock skew to timestamp validation -2. Mapping the response to a list of `GrantedAuthority` instances -3. Customizing the strategy for validating assertions -4. Customizing the strategy for decrypting response and assertion elements +1. Changing the way the `RelyingPartyRegistration` is Looked Up +2. Setting a clock skew to timestamp validation +3. Mapping the response to a list of `GrantedAuthority` instances +4. Customizing the strategy for validating assertions +5. Customizing the strategy for decrypting response and assertion elements To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL. +[[relyingpartyregistrationresolver-apply]] +== Changing `RelyingPartyRegistration` Lookup + +`RelyingPartyRegistration` lookup is customized xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-relyingpartyregistrationresolver[in a `RelyingPartyRegistrationResolver`]. + +To apply a `RelyingPartyRegistrationResolver` when processing `` payloads, you should first publish a `Saml2AuthenticationTokenConverter` bean like so: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +Saml2AuthenticationTokenConverter authenticationConverter(InMemoryRelyingPartyRegistrationRepository registrations) { + return new Saml2AuthenticationTokenConverter(new MyRelyingPartyRegistrationResolver(registrations)); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authenticationConverter(val registrations: InMemoryRelyingPartyRegistrationRepository): Saml2AuthenticationTokenConverter { + return Saml2AuthenticationTokenConverter(MyRelyingPartyRegistrationResolver(registrations)); +} +---- +==== + +Recall that the Assertion Consumer Service URL is `+/saml2/login/sso/{registrationId}+` by default. +If you are no longer wanting the `registrationId` in the URL, change it in the filter chain and in your relying party metadata: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +SecurityFilterChain securityFilters(HttpSecurity http) throws Exception { + http + // ... + .saml2Login((saml2) -> saml2.filterProcessingUrl("/saml2/login/sso")) + // ... + + return http.build(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun securityFilters(val http: HttpSecurity): SecurityFilterChain { + http { + // ... + .saml2Login { + filterProcessingUrl = "/saml2/login/sso" + } + // ... + } + + return http.build() +} +---- +==== + +and: + +==== +.Java +[source,java,role="primary"] +---- +relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso") +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml2/login/sso") +---- +==== + [[servlet-saml2login-opensamlauthenticationprovider-clockskew]] == Setting a Clock Skew diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index a527f0a925..d1d6c12f4f 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -33,6 +33,7 @@ image::{figures}/saml2webssoauthenticationfilter.png[] The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram. +[[servlet-saml2login-authentication-saml2authenticationtokenconverter]] image:{icondir}/number_1.png[] When the browser submits a `` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`]. This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`. This converter additionally resolves the <> and supplies it to `Saml2AuthenticationToken`. @@ -712,56 +713,6 @@ resource.inputStream.use { [TIP] When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you. -[[servlet-saml2login-rpr-relyingpartyregistrationresolver]] -=== Resolving the Relying Party from the Request - -As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path. - -There are a number of reasons you may want to customize. Among them: - -* You may know that you will never be a multi-tenant application and so want to have a simpler URL scheme -* You may identify tenants in a way other than by the URI path - -To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`. -The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`. - -You can provide a simpler resolver that, for example, always returns the same relying party: - -==== -.Java -[source,java,role="primary"] ----- -public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { - - private final RelyingPartyRegistrationResolver delegate; - - public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) { - this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations); - } - - @Override - public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { - return this.delegate.resolve(request, "single"); - } -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver { - override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? { - return this.delegate.resolve(request, "single") - } -} ----- -==== - -Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ````s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ````s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `` metadata]. - -[NOTE] -Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them. - [[servlet-saml2login-rpr-duplicated]] === Duplicated Relying Party Configurations @@ -856,3 +807,184 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? { } ---- ==== + +[[servlet-saml2login-rpr-relyingpartyregistrationresolver]] +=== Resolving the `RelyingPartyRegistration` from the Request + +As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path. + +There are a number of reasons you may want to customize that. Among them: + +* You may already <> +* You may be <> + +To customize the way that a `RelyingPartyRegistration` is resolved, you can configure a custom `RelyingPartyRegistrationResolver`. +The default looks up the registration id from the URI's last path element and looks it up in your `RelyingPartyRegistrationRepository`. + +[NOTE] +Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them. + +[[relyingpartyregistrationresolver-single]] +==== Resolving to a Single Consistent `RelyingPartyRegistration` + +You can provide a resolver that, for example, always returns the same `RelyingPartyRegistration`: + +==== +.Java +[source,java,role="primary"] +---- +public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { + + private final RelyingPartyRegistrationResolver delegate; + + public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) { + this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations); + } + + @Override + public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { + return this.delegate.resolve(request, "single"); + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver { + override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? { + return this.delegate.resolve(request, "single") + } +} +---- +==== + +[TIP] +You might next take a look at how to use this resolver to customize xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[`` metadata production]. + +[[relyingpartyregistrationresolver-entityid]] +==== Resolving Based on the `` + +When you have one relying party that can accept assertions from multiple asserting parties, you will have as many ``RelyingPartyRegistration``s as asserting parties, with the <>. + +This carries the implication that the assertion consumer service endpoint will be different for each asserting party, which may not be desirable. + +You can instead resolve the `registrationId` via the `Issuer`. +A custom implementation of `RelyingPartyRegistrationResolver` that does this may look like: + +==== +.Java +[source,java,role="primary"] +---- +public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver { + private final InMemoryRelyingPartyRegistrationRepository registrations; + + // ... constructor + + @Override + RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) { + if (registrationId != null) { + return this.registrations.findByRegistrationId(registrationId); + } + String entityId = resolveEntityIdFromSamlResponse(request); + for (RelyingPartyRegistration registration : this.registrations) { + if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) { + return registration; + } + } + return null; + } + + private String resolveEntityIdFromSamlResponse(HttpServletRequest request) { + // ... + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository): + RelyingPartyRegistrationResolver { + @Override + fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration { + if (registrationId != null) { + return this.registrations.findByRegistrationId(registrationId) + } + String entityId = resolveEntityIdFromSamlResponse(request) + for (val registration : this.registrations) { + if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) { + return registration + } + } + return null + } + + private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String { + // ... + } +} +---- +==== + +[TIP] +You might next take a look at how to use this resolver to customize xref:servlet/saml2/login/authentication.adoc#relyingpartyregistrationresolver-apply[`` authentication]. + +[[federating-saml2-login]] +=== Federating Login + +One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties. +In this case, the identity provider's metadata endpoint returns multiple `` elements. + +These multiple asserting parties can be accessed in a single call to `RelyingPartyRegistrations` like so: + +==== +.Java +[source,java,role="primary"] +---- +Collection registrations = RelyingPartyRegistrations + .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml") + .stream().map((builder) -> builder + .registrationId(UUID.randomUUID().toString()) + .entityId("https://example.org/saml2/sp") + .build() + ) + .collect(Collectors.toList())); +---- + +.Kotlin +[source,java,role="secondary"] +---- +var registrations: Collection = RelyingPartyRegistrations + .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml") + .stream().map { builder : RelyingPartyRegistration.Builder -> builder + .registrationId(UUID.randomUUID().toString()) + .entityId("https://example.org/saml2/sp") + .build() + } + .collect(Collectors.toList())); +---- +==== + +Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable. +There are several ways to address this; let's focus on a way that suits the specific use case of federation. + +In many federation cases, all the asserting parties share service provider configuration. +Given that Spring Security will by default include the `registrationId` in all many of its SAML 2.0 URIs, the next step is often to change these URIs to exclude the `registrationId`. + +There are two main URIs you will want to change along those lines: + +* <`>> +* <> + +[NOTE] +Optionally, you may also want to change the Authentication Request location, but since this is a URI internal to the app and not published to asserting parties, the benefit is often minimal. + +You can see a completed example of this in {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample]. + +[[using-spring-security-saml-extension-uris]] +=== Using Spring Security SAML Extension URIs + +In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults. + +For more information on this, please see {gh-samples-url}/servlet/spring-boot/java/saml2/custom-urls[our `custom-urls` sample] and {gh-samples-url}/servlet/spring-boot/java/saml2/saml-extension-federation[our `saml-extension-federation` sample]. diff --git a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc index 6f69ecec4f..81aa06c72d 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc @@ -103,3 +103,45 @@ filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET")); filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET")) ---- ==== + +== Changing the Way a `RelyingPartyRegistration` Is Looked Up + +To apply a custom `RelyingPartyRegistrationResolver` to the metadata endpoint, you can provide it directly in the filter constructor like so: + +==== +.Java +[source,java,role="primary"] +---- +RelyingPartyRegistrationResolver myRegistrationResolver = ...; +Saml2MetadataFilter metadata = new Saml2MetadataFilter(myRegistrationResolver, new OpenSamlMetadataResolver()); + +// ... + +http.addFilterBefore(metadata, BasicAuthenticationFilter.class); +---- + +.Kotlin +---- +val myRegistrationResolver: RelyingPartyRegistrationResolver = ...; +val metadata = new Saml2MetadataFilter(myRegistrationResolver, OpenSamlMetadataResolver()); + +// ... + +http.addFilterBefore(metadata, BasicAuthenticationFilter::class.java); +---- +==== + +In the event that you are applying a `RelyingPartyRegistrationResolver` to remove the `registrationId` from the URI, you must also change the URI in the filter like so: + +==== +.Java +[source,java,role="primary"] +---- +metadata.setRequestMatcher("/saml2/metadata") +---- + +.Kotlin +---- +metadata.setRequestMatcher("/saml2/metadata") +---- +==== From 0421e25cbaadad62ef7a7c7da67ab289b69b9361 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Tue, 28 Feb 2023 12:45:48 -0700 Subject: [PATCH 2/2] Document Common SAML URI Endpoints Issue gh-12764 --- .../ROOT/pages/servlet/saml2/login/overview.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index d1d6c12f4f..edd565a7e2 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -641,6 +641,16 @@ which in a deployed application would translate to `+https://rp.example.com/adfs+` +The prevailing URI patterns are as follows: + +* `+/saml2/authenticate/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication-requests.adoc[generates a ``] based on the configurations for that `RelyingPartyRegistration` and sends it to the asserting party +* `+/saml2/login/sso/{registrationId}+` - The endpoint that xref:servlet/saml2/login/authentication.adoc[authenticates an asserting party's ``] based on the configurations for that `RelyingPartyRegistration` +* `+/saml2/logout/sso+` - The endpoint that xref:servlet/saml2/logout.adoc[processes `` and `` payloads]; the `RelyingPartyRegistration` is looked up from previously authenticated state +* `+/saml2/saml2-service-provider/metadata/{registrationId}+` - The xref:servlet/saml2/metadata.adoc[relying party metadata] for that `RelyingPartyRegistration` + +Since the `registrationId` is the primary identifier for a `RelyingPartyRegistration`, it is needed in the URL for unauthenticated scenarios. +If you wish to remove the `registrationId` from the URL for any reason, you can <> to tell Spring Security how to look up the `registrationId`. + [[servlet-saml2login-rpr-credentials]] === Credentials