Procházet zdrojové kódy

Separate SAML Docs

Issue gh-10367
Josh Cummings před 3 roky
rodič
revize
f39d272a86

+ 3 - 0
docs/modules/ROOT/nav.adoc

@@ -63,6 +63,9 @@
 **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
 **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
 **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
 **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
 ** xref:servlet/saml2/index.adoc[SAML2]
 ** xref:servlet/saml2/index.adoc[SAML2]
+*** xref:servlet/saml2/login.adoc[SAML2 Log In]
+*** xref:servlet/saml2/logout.adoc[SAML2 Logout]
+*** xref:servlet/saml2/metadata.adoc[SAML2 Metadata]
 ** xref:servlet/exploits/index.adoc[Protection Against Exploits]
 ** xref:servlet/exploits/index.adoc[Protection Against Exploits]
 *** xref:servlet/exploits/csrf.adoc[]
 *** xref:servlet/exploits/csrf.adoc[]
 *** xref:servlet/exploits/headers.adoc[]
 *** xref:servlet/exploits/headers.adoc[]

+ 1 - 1612
docs/modules/ROOT/pages/servlet/saml2/index.adoc

@@ -1,1617 +1,6 @@
 [[servlet-saml2]]
 [[servlet-saml2]]
 = SAML2
 = SAML2
-:figures: servlet/saml2
+:page-section-summary-toc: 1
 
 
 Spring Security provides comprehensive SAML 2 support.
 Spring Security provides comprehensive SAML 2 support.
 This section discusses how to integrate SAML 2 into your servlet based application.
 This section discusses how to integrate SAML 2 into your servlet based application.
-
-[[servlet-saml2login]]
-== SAML 2.0 Login
-
-The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc).
-
-NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in
-https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles].
-
-[[servlet-saml2login-spring-security-history]]
-Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project].
-In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper.
-This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support].
-
-[NOTE]
-====
-A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository].
-====
-
-Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security.
-First, we see that, like xref:servlet/oauth2/oauth2-login.adoc#oauth2login[ OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication.
-It does this through a series of redirects.
-
-.Redirecting to Asserting Party Authentication
-image::{figures}/saml2webssoauthenticationrequestfilter.png[]
-
-The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] and xref:servlet/authentication/architecture.adoc#servlet-authentication-abstractprocessingfilter[ `AbstractAuthenticationProcessingFilter`] diagrams:
-
-image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized.
-
-image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
-
-image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__.
-The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <<servlet-saml2login-sp-initiated-factory,the `<saml2:AuthnRequest>` generating endpoint>>, `Saml2WebSsoAuthenticationRequestFilter`.
-Or, if you've <<servlet-saml2login-relyingpartyregistrationrepository,configured more than one asserting party>>, it will first redirect to a picker page.
-
-image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `<saml2:AuthnRequest>` using its configured <<servlet-saml2login-sp-initiated-factory,`Saml2AuthenticationRequestFactory`>>.
-
-image:{icondir}/number_5.png[] Then, the browser takes this `<saml2:AuthnRequest>` and presents it to the asserting party.
-The asserting party attempts to authentication the user.
-If successful, it will return a `<saml2:Response>` back to the browser.
-
-image:{icondir}/number_6.png[] The browser then POSTs the `<saml2:Response>` to the assertion consumer service endpoint.
-
-[[servlet-saml2login-authentication-saml2webssoauthenticationfilter]]
-.Authenticating a `<saml2:Response>`
-image::{figures}/saml2webssoauthenticationfilter.png[]
-
-The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
-
-image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it <<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 <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
-
-image:{icondir}/number_2.png[] Next, the filter passes the token to its configured xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
-By default, it will use the <<servlet-saml2login-architecture,`OpenSAML authentication provider`>>.
-
-image:{icondir}/number_3.png[] If authentication fails, then __Failure__
-
-* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`] is cleared out.
-* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is invoked to restart the authentication process.
-
-image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
-
-* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[ `Authentication`] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[ `SecurityContextHolder`].
-* The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
-
-[[servlet-saml2login-minimaldependencies]]
-=== Minimal Dependencies
-
-SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
-It builds off of the OpenSAML library.
-
-[[servlet-saml2login-minimalconfiguration]]
-=== Minimal Configuration
-
-When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps.
-First, include the needed dependencies and second, indicate the necessary asserting party metadata.
-
-[NOTE]
-Also, this presupposes that you've already <<servlet-saml2login-metadata, registered the relying party with your asserting party>>.
-
-==== Specifying Identity Provider Metadata
-
-In a Spring Boot application, to specify an identity provider's metadata, simply do:
-
-[source,yml]
-----
-spring:
-  security:
-    saml2:
-      relyingparty:
-        registration:
-          adfs:
-            identityprovider:
-              entity-id: https://idp.example.com/issuer
-              verification.credentials:
-                - certificate-location: "classpath:idp.crt"
-              singlesignon.url: https://idp.example.com/issuer/sso
-              singlesignon.sign-request: false
-----
-
-where
-
-* `https://idp.example.com/issuer` is the value contained in the `Issuer` attribute of the SAML responses that the identity provider will issue
-* `classpath:idp.crt` is the location on the classpath for the identity provider's certificate for verifying SAML responses, and
-* `https://idp.example.com/issuer/sso` is the endpoint where the identity provider is expecting `AuthnRequest` s.
-
-And that's it!
-
-[NOTE]
-Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
-These are frequently abbreviated as AP and RP, respectively.
-
-==== Runtime Expectations
-
-As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
-
-[source,html]
-----
-POST /login/saml2/sso/adfs HTTP/1.1
-
-SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
-----
-
-There are two ways to see induce your asserting party to generate a `SAMLResponse`:
-
-* First, you can navigate to your asserting party.
-It likely has some kind of link or button for each registered relying party that you can click to send the `SAMLResponse`.
-* Second, you can navigate to a protected page in your app, for example, `http://localhost:8080`.
-Your app then redirects to the configured asserting party which then sends the `SAMLResponse`.
-
-From here, consider jumping to:
-
-* <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
-* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
-* <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
-
-[[servlet-saml2login-architecture]]
-=== How SAML 2.0 Login Integrates with OpenSAML
-
-Spring Security's SAML 2.0 support has a couple of design goals:
-
-* First, rely on a library for SAML 2.0 operations and domain objects.
-To achieve this, Spring Security uses OpenSAML.
-* Second, ensure this library is not required when using Spring Security's SAML support.
-To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated.
-This makes it possible for you to switch out OpenSAML for some other library or even an unsupported version of OpenSAML.
-
-As a natural outcome of the above two goals, Spring Security's SAML API is quite small relative to other modules.
-Instead, classes like `OpenSaml4AuthenticationRequestFactory` and `OpenSaml4AuthenticationProvider` expose `Converter` s that customize various steps in the authentication process.
-
-For example, once your application receives a `SAMLResponse` and delegates to `Saml2WebSsoAuthenticationFilter`, the filter will delegate to `OpenSaml4AuthenticationProvider`.
-
-[NOTE]
-For backward compatibility, Spring Security will use the latest OpenSAML 3 by default.
-Note, though that OpenSAML 3 has reached it's end-of-life and updating to OpenSAML 4.x is recommended.
-For that reason, Spring Security supports both OpenSAML 3.x and 4.x.
-If you manage your OpenSAML dependency to 4.x, then Spring Security will select its OpenSAML 4.x implementations.
-
-.Authenticating an OpenSAML `Response`
-image:{figures}/opensamlauthenticationprovider.png[]
-
-This figure builds off of the <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter` diagram>>.
-
-image:{icondir}/number_1.png[] The `Saml2WebSsoAuthenticationFilter` formulates the `Saml2AuthenticationToken` and invokes the xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`].
-
-image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc#servlet-authentication-providermanager[`AuthenticationManager`] invokes the OpenSAML authentication provider.
-
-image:{icondir}/number_3.png[] The authentication provider deserializes the response into an OpenSAML `Response` and checks its signature.
-If the signature is invalid, authentication fails.
-
-image:{icondir}/number_4.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedAssertion` elements>>.
-If any decryptions fail, authentication fails.
-
-image:{icondir}/number_5.png[] Next, the provider validates the response's `Issuer` and `Destination` values.
-If they don't match what's in the `RelyingPartyRegistration`, authentication fails.
-
-image:{icondir}/number_6.png[] After that, the provider verifies the signature of each `Assertion`.
-If any signature is invalid, authentication fails.
-Also, if neither the response nor the assertions have signatures, authentication fails.
-Either the response or all the assertions must have signatures.
-
-image:{icondir}/number_7.png[] Then, the provider <<servlet-saml2login-opensamlauthenticationprovider-decryption,decrypts any `EncryptedID` or `EncryptedAttribute` elements>>.
-If any decryptions fail, authentication fails.
-
-image:{icondir}/number_8.png[] Next, the provider validates each assertion's `ExpiresAt` and `NotBefore` timestamps, the `<Subject>` and any `<AudienceRestriction>` conditions.
-If any validations fail, authentication fails.
-
-image:{icondir}/number_9.png[] Following that, the provider takes the first assertion's `AttributeStatement` and maps it to a `Map<String, List<Object>>`.
-It also grants the `ROLE_USER` granted authority.
-
-image:{icondir}/number_10.png[] And finally, it takes the `NameID` from the first assertion, the `Map` of attributes, and the `GrantedAuthority` and constructs a `Saml2AuthenticatedPrincipal`.
-Then, it places that principal and the authorities into a `Saml2Authentication`.
-
-The resulting `Authentication#getPrincipal` is a Spring Security `Saml2AuthenticatedPrincipal` object, and `Authentication#getName` maps to the first assertion's `NameID` element.
-
-[[servlet-saml2login-opensaml-customization]]
-==== Customizing OpenSAML Configuration
-
-Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-static {
-	OpenSamlInitializationService.initialize();
-}
-----
-
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-companion object {
-    init {
-        OpenSamlInitializationService.initialize()
-    }
-}
-----
-====
-
-This replaces OpenSAML's `InitializationService#initialize`.
-
-Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
-In these circumstances, you may instead want to call `OpenSamlInitializationService#requireInitialize(Consumer)` that gives you access to OpenSAML's `XMLObjectProviderFactory`.
-
-For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
-In that case, you can register your own `AuthnRequestMarshaller`, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-static {
-    OpenSamlInitializationService.requireInitialize(factory -> {
-        AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
-            @Override
-            public Element marshall(XMLObject object, Element element) throws MarshallingException {
-                configureAuthnRequest((AuthnRequest) object);
-                return super.marshall(object, element);
-            }
-
-            public Element marshall(XMLObject object, Document document) throws MarshallingException {
-                configureAuthnRequest((AuthnRequest) object);
-                return super.marshall(object, document);
-            }
-
-            private void configureAuthnRequest(AuthnRequest authnRequest) {
-                authnRequest.setForceAuthn(true);
-            }
-        }
-
-        factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
-    });
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-companion object {
-    init {
-        OpenSamlInitializationService.requireInitialize {
-            val marshaller = object : AuthnRequestMarshaller() {
-                override fun marshall(xmlObject: XMLObject, element: Element): Element {
-                    configureAuthnRequest(xmlObject as AuthnRequest)
-                    return super.marshall(xmlObject, element)
-                }
-
-                override fun marshall(xmlObject: XMLObject, document: Document): Element {
-                    configureAuthnRequest(xmlObject as AuthnRequest)
-                    return super.marshall(xmlObject, document)
-                }
-
-                private fun configureAuthnRequest(authnRequest: AuthnRequest) {
-                    authnRequest.isForceAuthn = true
-                }
-            }
-            it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
-        }
-    }
-}
-----
-====
-
-The `requireInitialize` method may only be called once per application instance.
-
-[[servlet-saml2login-sansboot]]
-=== Overriding or Replacing Boot Auto Configuration
-
-There are two `@Bean` s that Spring Boot generates for a relying party.
-
-The first is a `WebSecurityConfigurerAdapter` that configures the app as a relying party.
-When including `spring-security-saml2-service-provider`, the `WebSecurityConfigurerAdapter` looks like:
-
-.Default JWT Configuration
-====
-.Java
-[source,java,role="primary"]
-----
-protected void configure(HttpSecurity http) {
-    http
-        .authorizeRequests(authorize -> authorize
-            .anyRequest().authenticated()
-        )
-        .saml2Login(withDefaults());
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-fun configure(http: HttpSecurity) {
-    http {
-        authorizeRequests {
-            authorize(anyRequest, authenticated)
-        }
-        saml2Login { }
-    }
-}
-----
-====
-
-If the application doesn't expose a `WebSecurityConfigurerAdapter` bean, then Spring Boot will expose the above default one.
-
-You can replace this by exposing the bean within the application:
-
-.Custom SAML 2.0 Login Configuration
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebSecurity
-public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
-    protected void configure(HttpSecurity http) {
-        http
-            .authorizeRequests(authorize -> authorize
-                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
-                .anyRequest().authenticated()
-            )
-            .saml2Login(withDefaults());
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@EnableWebSecurity
-class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
-    override fun configure(http: HttpSecurity) {
-        http {
-            authorizeRequests {
-                authorize("/messages/**", hasAuthority("ROLE_USER"))
-                authorize(anyRequest, authenticated)
-            }
-            saml2Login {
-            }
-        }
-    }
-}
-----
-====
-
-The above requires the role of `USER` for any URL that starts with `/messages/`.
-
-[[servlet-saml2login-relyingpartyregistrationrepository]]
-The second `@Bean` Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository`], which represents the asserting party and relying party metadata.
-This includes things like the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.
-
-You can override the default by publishing your own `RelyingPartyRegistrationRepository` bean.
-For example, you can look up the asserting party's configuration by hitting its metadata endpoint like so:
-
-.Relying Party Registration Repository
-====
-.Java
-[source,java,role="primary"]
-----
-@Value("${metadata.location}")
-String assertingPartyMetadataLocation;
-
-@Bean
-public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
-    RelyingPartyRegistration registration = RelyingPartyRegistrations
-            .fromMetadataLocation(assertingPartyMetadataLocation)
-            .registrationId("example")
-            .build();
-    return new InMemoryRelyingPartyRegistrationRepository(registration);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Value("\${metadata.location}")
-var assertingPartyMetadataLocation: String? = null
-
-@Bean
-open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
-    val registration = RelyingPartyRegistrations
-        .fromMetadataLocation(assertingPartyMetadataLocation)
-        .registrationId("example")
-        .build()
-    return InMemoryRelyingPartyRegistrationRepository(registration)
-}
-----
-====
-
-Or you can provide each detail manually, as you can see below:
-
-.Relying Party Registration Repository Manual Configuration
-====
-.Java
-[source,java,role="primary"]
-----
-@Value("${verification.key}")
-File verificationKey;
-
-@Bean
-public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
-    X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
-    Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
-    RelyingPartyRegistration registration = RelyingPartyRegistration
-            .withRegistrationId("example")
-            .assertingPartyDetails(party -> party
-                .entityId("https://idp.example.com/issuer")
-                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
-                .wantAuthnRequestsSigned(false)
-                .verificationX509Credentials(c -> c.add(credential))
-            )
-            .build();
-    return new InMemoryRelyingPartyRegistrationRepository(registration);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Value("\${verification.key}")
-var verificationKey: File? = null
-
-@Bean
-open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
-    val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
-    val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
-    val registration = RelyingPartyRegistration
-        .withRegistrationId("example")
-        .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
-            party
-                .entityId("https://idp.example.com/issuer")
-                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
-                .wantAuthnRequestsSigned(false)
-                .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
-                    c.add(
-                        credential
-                    )
-                }
-        }
-        .build()
-    return InMemoryRelyingPartyRegistrationRepository(registration)
-}
-----
-====
-
-[NOTE]
-Note that `X509Support` is an OpenSAML class, used here in the snippet for brevity
-
-[[servlet-saml2login-relyingpartyregistrationrepository-dsl]]
-
-Alternatively, you can directly wire up the repository using the DSL, which will also override the auto-configured `WebSecurityConfigurerAdapter`:
-
-.Custom Relying Party Registration DSL
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebSecurity
-public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
-    protected void configure(HttpSecurity http) {
-        http
-            .authorizeRequests(authorize -> authorize
-                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
-                .anyRequest().authenticated()
-            )
-            .saml2Login(saml2 -> saml2
-                .relyingPartyRegistrationRepository(relyingPartyRegistrations())
-            );
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@EnableWebSecurity
-class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
-    override fun configure(http: HttpSecurity) {
-        http {
-            authorizeRequests {
-                authorize("/messages/**", hasAuthority("ROLE_USER"))
-                authorize(anyRequest, authenticated)
-            }
-            saml2Login {
-                relyingPartyRegistrationRepository = relyingPartyRegistrations()
-            }
-        }
-    }
-}
-----
-====
-
-[NOTE]
-A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
-
-[[servlet-saml2login-relyingpartyregistration]]
-=== RelyingPartyRegistration
-A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
-instance represents a link between an relying party and assering party's metadata.
-
-In a `RelyingPartyRegistration`, you can provide relying party metadata like its `Issuer` value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.
-
-Also, you can provide asserting party metadata like its `Issuer` value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.
-
-The following `RelyingPartyRegistration` is the minimum required for most setups:
-
-====
-.Java
-[source,java,role="primary"]
-----
-RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
-        .fromMetadataLocation("https://ap.example.org/metadata")
-        .registrationId("my-id")
-        .build();
-----
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val relyingPartyRegistration = RelyingPartyRegistrations
-    .fromMetadataLocation("https://ap.example.org/metadata")
-    .registrationId("my-id")
-    .build()
-----
-====
-
-Note that you can also create a `RelyingPartyRegistration` from an arbitrary `InputStream` source.
-One such example is when the metadata is stored in a database:
-
-[source,java]
-----
-String xml = fromDatabase();
-try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
-    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
-            .fromMetadata(source)
-            .registrationId("my-id")
-            .build();
-}
-----
-
-Though a more sophisticated setup is also possible, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
-        .entityId("{baseUrl}/{registrationId}")
-        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
-        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
-        .assertingPartyDetails(party -> party
-                .entityId("https://ap.example.org")
-                .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
-                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
-        )
-        .build();
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val relyingPartyRegistration =
-    RelyingPartyRegistration.withRegistrationId("my-id")
-        .entityId("{baseUrl}/{registrationId}")
-        .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
-            c.add(relyingPartyDecryptingCredential())
-        }
-        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
-        .assertingPartyDetails { party -> party
-                .entityId("https://ap.example.org")
-                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
-                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
-        }
-        .build()
-----
-====
-
-[TIP]
-The top-level metadata methods are details about the relying party.
-The methods inside `assertingPartyDetails` are details about the asserting party.
-
-[NOTE]
-The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.
-
-The default for the relying party's `entityId` is `+{baseUrl}/saml2/service-provider-metadata/{registrationId}+`.
-This is this value needed when configuring the asserting party to know about your relying party.
-
-The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{registrationId}+`.
-It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
-
-[[servlet-saml2login-rpr-uripatterns]]
-==== URI Patterns
-
-You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
-
-These are useful for generating URIs. As such, the relying party's `entityId` and `assertionConsumerServiceLocation` support the following placeholders:
-
-* `baseUrl` - the scheme, host, and port of a deployed application
-* `registrationId` - the registration id for this relying party
-* `baseScheme` - the scheme of a deployed application
-* `baseHost` - the host of a deployed application
-* `basePort` - the port of a deployed application
-
-For example, the `assertionConsumerServiceLocation` defined above was:
-
-`+/my-login-endpoint/{registrationId}+`
-
-which in a deployed application would translate to
-
-`+/my-login-endpoint/adfs+`
-
-The `entityId` above was defined as:
-
-`+{baseUrl}/{registrationId}+`
-
-which in a deployed application would translate to
-
-`+https://rp.example.com/adfs+`
-
-[[servlet-saml2login-rpr-credentials]]
-==== Credentials
-
-You also likely noticed the credential that was used.
-
-Oftentimes, a relying party will use the same key to sign payloads as well as decrypt them.
-Or it will use the same key to verify payloads as well as encrypt them.
-
-Because of this, Spring Security ships with `Saml2X509Credential`, a SAML-specific credential that simplifies configuring the same key for different use cases.
-
-At a minimum, it's necessary to have a certificate from the asserting party so that the asserting party's signed responses can be verified.
-
-To construct a `Saml2X509Credential` that you'll use to verify assertions from the asserting party, you can load the file and use
-the `CertificateFactory` like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-Resource resource = new ClassPathResource("ap.crt");
-try (InputStream is = resource.getInputStream()) {
-    X509Certificate certificate = (X509Certificate)
-            CertificateFactory.getInstance("X.509").generateCertificate(is);
-    return Saml2X509Credential.verification(certificate);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val resource = ClassPathResource("ap.crt")
-resource.inputStream.use {
-    return Saml2X509Credential.verification(
-        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
-    )
-}
-----
-====
-
-Let's say that the asserting party is going to also encrypt the assertion.
-In that case, the relying party will need a private key to be able to decrypt the encrypted value.
-
-In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
-You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
-
-====
-.Java
-[source,java,role="primary"]
-----
-X509Certificate certificate = relyingPartyDecryptionCertificate();
-Resource resource = new ClassPathResource("rp.crt");
-try (InputStream is = resource.getInputStream()) {
-    RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
-    return Saml2X509Credential.decryption(rsa, certificate);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val certificate: X509Certificate = relyingPartyDecryptionCertificate()
-val resource = ClassPathResource("rp.crt")
-resource.inputStream.use {
-    val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
-    return Saml2X509Credential.decryption(rsa, certificate)
-}
-----
-====
-
-[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 `Converter<HttpServletRequest, RelyingPartyRegistration>`.
-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 Converter<HttpServletRequest, RelyingPartyRegistration> {
-
-    @Override
-    public RelyingPartyRegistration convert(HttpServletRequest request) {
-        return this.relyingParty;
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
-    override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
-        return this.relyingParty
-    }
-}
-----
-====
-
-Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce `<saml2:AuthnRequest>` s>>, <<servlet-saml2login-authenticate-responses, authenticate `<saml2:Response>` s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` 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
-
-When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
-
-* The relying party's `entityId`
-* Its `assertionConsumerServiceLocation`, and
-* Its credentials, for example its signing or decryption credentials
-
-What's nice about this setup is credentials may be more easily rotated for some identity providers vs others.
-
-The duplication can be alleviated in a few different ways.
-
-First, in YAML this can be alleviated with references, like so:
-
-[source,yaml]
-----
-spring:
-  security:
-    saml2:
-      relyingparty:
-        okta:
-          signing.credentials: &relying-party-credentials
-            - private-key-location: classpath:rp.key
-            - certificate-location: classpath:rp.crt
-          identityprovider:
-            entity-id: ...
-        azure:
-          signing.credentials: *relying-party-credentials
-          identityprovider:
-            entity-id: ...
-----
-
-Second, in a database, it's not necessary to replicate `RelyingPartyRegistration` 's model.
-
-Third, in Java, you can create a custom configuration method, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-private RelyingPartyRegistration.Builder
-        addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
-
-    Saml2X509Credential signingCredential = ...
-    builder.signingX509Credentials(c -> c.addAll(signingCredential));
-    // ... other relying party configurations
-}
-
-@Bean
-public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
-    RelyingPartyRegistration okta = addRelyingPartyDetails(
-            RelyingPartyRegistrations
-                .fromMetadataLocation(oktaMetadataUrl)
-                .registrationId("okta")).build();
-
-    RelyingPartyRegistration azure = addRelyingPartyDetails(
-            RelyingPartyRegistrations
-                .fromMetadataLocation(oktaMetadataUrl)
-                .registrationId("azure")).build();
-
-    return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
-    val signingCredential: Saml2X509Credential = ...
-    builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
-        c.add(
-            signingCredential
-        )
-    }
-    // ... other relying party configurations
-}
-
-@Bean
-open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
-    val okta = addRelyingPartyDetails(
-        RelyingPartyRegistrations
-            .fromMetadataLocation(oktaMetadataUrl)
-            .registrationId("okta")
-    ).build()
-    val azure = addRelyingPartyDetails(
-        RelyingPartyRegistrations
-            .fromMetadataLocation(oktaMetadataUrl)
-            .registrationId("azure")
-    ).build()
-    return InMemoryRelyingPartyRegistrationRepository(okta, azure)
-}
-----
-====
-
-[[servlet-saml2login-sp-initiated-factory]]
-=== Producing `<saml2:AuthnRequest>` s
-
-As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
-
-Spring Security achieves this in part by registering the `Saml2WebSsoAuthenticationRequestFilter` in the filter chain.
-This filter by default responds to endpoint `+/saml2/authenticate/{registrationId}+`.
-
-For example, if you were deployed to `https://rp.example.com` and you gave your registration an ID of `okta`, you could navigate to:
-
-`https://rp.example.org/saml2/authenticate/ping`
-
-and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
-
-[[servlet-saml2login-sp-initiated-factory-signing]]
-==== Changing How the `<saml2:AuthnRequest>` Gets Sent
-
-By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
-
-Many asserting parties don't require a signed `<saml2:AuthnRequest>`.
-This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
-
-
-.Not Requiring Signed AuthnRequests
-====
-.Boot
-[source,yaml,role="primary"]
-----
-spring:
-  security:
-    saml2:
-      relyingparty:
-        okta:
-          identityprovider:
-            entity-id: ...
-            singlesignon.sign-request: false
-----
-
-.Java
-[source,java,role="secondary"]
-----
-RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
-        // ...
-        .assertingPartyDetails(party -> party
-            // ...
-            .wantAuthnRequestsSigned(false)
-        )
-        .build();
-----
-
-.Kotlin
-[source,java,role="secondary"]
-----
-var relyingPartyRegistration: RelyingPartyRegistration =
-    RelyingPartyRegistration.withRegistrationId("okta")
-        // ...
-        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
-                // ...
-                .wantAuthnRequestsSigned(false)
-        }
-        .build();
-----
-====
-
-Otherwise, you will need to specify a private key to `RelyingPartyRegistration#signingX509Credentials` so that Spring Security can sign the `<saml2:AuthnRequest>` before sending.
-
-[[servlet-saml2login-sp-initiated-factory-algorithm]]
-By default, Spring Security will sign the `<saml2:AuthnRequest>` using `rsa-sha256`, though some asserting parties will require a different algorithm, as indicated in their metadata.
-
-You can configure the algorithm based on the asserting party's <<servlet-saml2login-relyingpartyregistrationrepository,metadata using `RelyingPartyRegistrations`>>.
-
-Or, you can provide it manually:
-
-====
-.Java
-[source,java,role="primary"]
-----
-String metadataLocation = "classpath:asserting-party-metadata.xml";
-RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
-        // ...
-        .assertingPartyDetails((party) -> party
-            // ...
-            .signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
-        )
-        .build();
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-var metadataLocation = "classpath:asserting-party-metadata.xml"
-var relyingPartyRegistration: RelyingPartyRegistration =
-    RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
-        // ...
-        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
-                // ...
-                .signingAlgorithms { sign: MutableList<String?> ->
-                    sign.add(
-                        SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
-                    )
-                }
-        }
-        .build();
-----
-====
-
-NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
-But, that's just for convenience.
-Since the datatype is `String`, you can supply the name of the algorithm directly.
-
-[[servlet-saml2login-sp-initiated-factory-binding]]
-Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
-This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
-        // ...
-        .assertingPartyDetails(party -> party
-            // ...
-            .singleSignOnServiceBinding(Saml2MessageBinding.POST)
-        )
-        .build();
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-var relyingPartyRegistration: RelyingPartyRegistration? =
-    RelyingPartyRegistration.withRegistrationId("okta")
-        // ...
-        .assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
-            // ...
-            .singleSignOnServiceBinding(Saml2MessageBinding.POST)
-        }
-        .build()
-----
-====
-
-[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
-==== Customizing OpenSAML's `AuthnRequest` Instance
-
-There are a number of reasons that you may want to adjust an `AuthnRequest`.
-For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
-
-If you don't need information from the `HttpServletRequest` to make your decision, then the easiest way is to <<servlet-saml2login-opensaml-customization,register a custom `AuthnRequestMarshaller` with OpenSAML>>.
-This will give you access to post-process the `AuthnRequest` instance before it's serialized.
-
-But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Component
-public class AuthnRequestConverter implements
-        Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
-
-    private final AuthnRequestBuilder authnRequestBuilder;
-    private final IssuerBuilder issuerBuilder;
-
-    // ... constructor
-
-    public AuthnRequest convert(Saml2AuthenticationRequestContext context) {
-        MySaml2AuthenticationRequestContext myContext = (MySaml2AuthenticationRequestContext) context;
-        Issuer issuer = issuerBuilder.buildObject();
-        issuer.setValue(myContext.getIssuer());
-
-        AuthnRequest authnRequest = authnRequestBuilder.buildObject();
-        authnRequest.setIssuer(issuer);
-        authnRequest.setDestination(myContext.getDestination());
-        authnRequest.setAssertionConsumerServiceURL(myContext.getAssertionConsumerServiceUrl());
-
-        // ... additional settings
-
-        authRequest.setForceAuthn(myContext.getForceAuthn());
-        return authnRequest;
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Component
-class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
-    private val authnRequestBuilder: AuthnRequestBuilder? = null
-    private val issuerBuilder: IssuerBuilder? = null
-
-    // ... constructor
-    override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
-        val myContext: MySaml2AuthenticationRequestContext = context
-        val issuer: Issuer = issuerBuilder.buildObject()
-        issuer.value = myContext.getIssuer()
-        val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
-        authnRequest.issuer = issuer
-        authnRequest.destination = myContext.getDestination()
-        authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
-
-        // ... additional settings
-        authRequest.setForceAuthn(myContext.getForceAuthn())
-        return authnRequest
-    }
-}
-----
-====
-
-Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as `@Bean` s:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
-    Saml2AuthenticationRequestContextResolver resolver =
-            new DefaultSaml2AuthenticationRequestContextResolver();
-    return request -> {
-        Saml2AuthenticationRequestContext context = resolver.resolve(request);
-        return new MySaml2AuthenticationRequestContext(context, request.getParameter("force") != null);
-    };
-}
-
-@Bean
-Saml2AuthenticationRequestFactory authenticationRequestFactory(
-        AuthnRequestConverter authnRequestConverter) {
-
-    OpenSaml4AuthenticationRequestFactory authenticationRequestFactory =
-            new OpenSaml4AuthenticationRequestFactory();
-    authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
-    return authenticationRequestFactory;
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
-    val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
-    return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
-        val context = resolver.resolve(request)
-        MySaml2AuthenticationRequestContext(
-            context,
-            request.getParameter("force") != null
-        )
-    }
-}
-
-@Bean
-open fun authenticationRequestFactory(
-    authnRequestConverter: AuthnRequestConverter?
-): Saml2AuthenticationRequestFactory? {
-    val authenticationRequestFactory = OpenSaml4AuthenticationRequestFactory()
-    authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
-    return authenticationRequestFactory
-}
-----
-====
-
-[[servlet-saml2login-authenticate-responses]]
-=== Authenticating `<saml2:Response>` s
-
-To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
-
-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
-
-To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
-
-[[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
-==== Setting a Clock Skew
-
-It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
-For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebSecurity
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-    @Override
-    protected void configure(HttpSecurity http) throws Exception {
-        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
-        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
-                .createDefaultAssertionValidator(assertionToken -> {
-                    Map<String, Object> params = new HashMap<>();
-                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
-                    // ... other validation parameters
-                    return new ValidationContext(params);
-                })
-        );
-
-        http
-            .authorizeRequests(authz -> authz
-                .anyRequest().authenticated()
-            )
-            .saml2Login(saml2 -> saml2
-                .authenticationManager(new ProviderManager(authenticationProvider))
-            );
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@EnableWebSecurity
-open class SecurityConfig : WebSecurityConfigurerAdapter() {
-    override fun configure(http: HttpSecurity) {
-        val authenticationProvider = OpenSaml4AuthenticationProvider()
-        authenticationProvider.setAssertionValidator(
-            OpenSaml4AuthenticationProvider
-                .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
-                    val params: MutableMap<String, Any> = HashMap()
-                    params[CLOCK_SKEW] =
-                        Duration.ofMinutes(10).toMillis()
-                    ValidationContext(params)
-                })
-        )
-        http {
-            authorizeRequests {
-                authorize(anyRequest, authenticated)
-            }
-            saml2Login {
-                authenticationManager = ProviderManager(authenticationProvider)
-            }
-        }
-    }
-}
-----
-====
-
-[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
-==== Coordinating with a `UserDetailsService`
-
-Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
-In that case, the response authentication converter can come in handy, as can be seen below:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebSecurity
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
-    @Autowired
-    UserDetailsService userDetailsService;
-
-    @Override
-    protected void configure(HttpSecurity http) throws Exception {
-        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
-        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
-            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
-                    .createDefaultResponseAuthenticationConverter() <1>
-                    .convert(responseToken);
-            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
-            String username = assertion.getSubject().getNameID().getValue();
-            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); <2>
-            return MySaml2Authentication(userDetails, authentication); <3>
-        });
-
-        http
-            .authorizeRequests(authz -> authz
-                .anyRequest().authenticated()
-            )
-            .saml2Login(saml2 -> saml2
-                .authenticationManager(new ProviderManager(authenticationProvider))
-            );
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@EnableWebSecurity
-open class SecurityConfig : WebSecurityConfigurerAdapter() {
-    @Autowired
-    var userDetailsService: UserDetailsService? = null
-
-    override fun configure(http: HttpSecurity) {
-        val authenticationProvider = OpenSaml4AuthenticationProvider()
-        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
-            val authentication = OpenSaml4AuthenticationProvider
-                .createDefaultResponseAuthenticationConverter() <1>
-                .convert(responseToken)
-            val assertion: Assertion = responseToken.response.assertions[0]
-            val username: String = assertion.subject.nameID.value
-            val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
-            MySaml2Authentication(userDetails, authentication) <3>
-        }
-        http {
-            authorizeRequests {
-                authorize(anyRequest, authenticated)
-            }
-            saml2Login {
-                authenticationManager = ProviderManager(authenticationProvider)
-            }
-        }
-    }
-}
-----
-====
-<1> First, call the default converter, which extracts attributes and authorities from the response
-<2> Second, call the xref:servlet/authentication/passwords/user-details-service.adoc#servlet-authentication-userdetailsservice[ `UserDetailsService`] using the relevant information
-<3> Third, return a custom authentication that includes the user details
-
-[NOTE]
-It's not required to call `OpenSaml4AuthenticationProvider` 's default authentication converter.
-It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from `AttributeStatement` s as well as the single `ROLE_USER` authority.
-
-[[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
-==== Performing Additional Response Validation
-
-`OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
-You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
-
-For example, you can throw a custom exception with any additional information available in the `Response` object, like so:
-[source,java]
-----
-OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
-provider.setResponseValidator((responseToken) -> {
-	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
-		.createDefaultResponseValidator()
-		.convert(responseToken)
-		.concat(myCustomValidator.convert(responseToken));
-	if (!result.getErrors().isEmpty()) {
-		String inResponseTo = responseToken.getInResponseTo();
-		throw new CustomSaml2AuthenticationException(result, inResponseTo);
-	}
-	return result;
-});
-----
-
-==== Performing Additional Assertion Validation
-`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
-After verifying the signature, it will:
-
-1. Validate `<AudienceRestriction>` and `<DelegationRestriction>` conditions
-2. Validate `<SubjectConfirmation>` s, expect for any IP address information
-
-To perform additional validation, you can configure your own assertion validator that delegates to `OpenSaml4AuthenticationProvider` 's default and then performs its own.
-
-[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
-For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
-OneTimeUseConditionValidator validator = ...;
-provider.setAssertionValidator(assertionToken -> {
-    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
-            .createDefaultAssertionValidator()
-            .convert(assertionToken);
-    Assertion assertion = assertionToken.getAssertion();
-    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
-    ValidationContext context = new ValidationContext();
-    try {
-        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
-            return result;
-        }
-    } catch (Exception e) {
-        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
-    }
-    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
-});
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-var provider = OpenSaml4AuthenticationProvider()
-var validator: OneTimeUseConditionValidator = ...
-provider.setAssertionValidator { assertionToken ->
-    val result = OpenSaml4AuthenticationProvider
-        .createDefaultAssertionValidator()
-        .convert(assertionToken)
-    val assertion: Assertion = assertionToken.assertion
-    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
-    val context = ValidationContext()
-    try {
-        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
-            return@setAssertionValidator result
-        }
-    } catch (e: Exception) {
-        return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
-    }
-    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
-}
-----
-====
-
-[NOTE]
-While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider` 's default assertion validator.
-A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
-
-[[servlet-saml2login-opensamlauthenticationprovider-decryption]]
-==== Customizing Decryption
-
-Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
-
-`OpenSaml4AuthenticationProvider` exposes <<servlet-saml2login-architecture,two decryption strategies>>.
-The response decrypter is for decrypting encrypted elements of the `<saml2:Response>`, like `<saml2:EncryptedAssertion>`.
-The assertion decrypter is for decrypting encrypted elements of the `<saml2:Assertion>`, like `<saml2:EncryptedAttribute>` and `<saml2:EncryptedID>`.
-
-You can replace `OpenSaml4AuthenticationProvider`'s default decryption strategy with your own.
-For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-MyDecryptionService decryptionService = ...;
-OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
-provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val decryptionService: MyDecryptionService = ...
-val provider = OpenSaml4AuthenticationProvider()
-provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
-----
-====
-
-If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
-
-====
-.Java
-[source,java,role="primary"]
-----
-provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
-----
-====
-
-NOTE: There are two separate decrypters since assertions can be signed separately from responses.
-Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
-If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
-
-[[servlet-saml2login-authenticationmanager-custom]]
-==== Using a Custom Authentication Manager
-
-[[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
-Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
-This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
-
-====
-.Java
-[source,java,role="primary"]
-----
-@EnableWebSecurity
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
-
-    @Override
-    protected void configure(HttpSecurity http) throws Exception {
-        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
-        http
-            .authorizeRequests(authorize -> authorize
-                .anyRequest().authenticated()
-            )
-            .saml2Login(saml2 -> saml2
-                .authenticationManager(authenticationManager)
-            )
-        ;
-    }
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@EnableWebSecurity
-open class SecurityConfig : WebSecurityConfigurerAdapter() {
-    override fun configure(http: HttpSecurity) {
-        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
-        http {
-            authorizeRequests {
-                authorize(anyRequest, authenticated)
-            }
-            saml2Login {
-                authenticationManager = customAuthenticationManager
-            }
-        }
-    }
-}
-----
-====
-
-[[servlet-saml2login-authenticatedprincipal]]
-=== Using `Saml2AuthenticatedPrincipal`
-
-With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
-Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
-
-This means that you can access the principal in your controller like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Controller
-public class MainController {
-	@GetMapping("/")
-	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
-		String email = principal.getFirstAttribute("email");
-		model.setAttribute("email", email);
-		return "index";
-	}
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Controller
-class MainController {
-    @GetMapping("/")
-    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
-        val email = principal.getFirstAttribute<String>("email")
-        model.setAttribute("email", email)
-        return "index"
-    }
-}
-----
-====
-
-[TIP]
-Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
-`getFirstAttribute` is quite handy when you know that there is only one value.
-
-[[servlet-saml2login-metadata]]
-=== Producing `<saml2:SPSSODescriptor>` Metadata
-
-You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
-
-====
-.Java
-[source,java,role="primary"]
-----
-Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
-        new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
-Saml2MetadataFilter filter = new Saml2MetadataFilter(
-        relyingPartyRegistrationResolver,
-        new OpenSamlMetadataResolver());
-
-http
-    // ...
-    .saml2Login(withDefaults())
-    .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
-    DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
-val filter = Saml2MetadataFilter(
-    relyingPartyRegistrationResolver,
-    OpenSamlMetadataResolver()
-)
-
-http {
-    //...
-    saml2Login { }
-    addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
-}
-----
-====
-
-You can use this metadata endpoint to register your relying party with your asserting party.
-This is often as simple as finding the correct form field to supply the metadata endpoint.
-
-By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
-You can change this by calling the `setRequestMatcher` method on the filter:
-
-====
-.Java
-[source,java,role="primary"]
-----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
-----
-====
-
-ensuring that the `registrationId` hint is at the end of the path.
-
-Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
-----
-====
-
-[[servlet-saml2login-logout]]
-=== Performing Single Logout
-
-Spring Security does not yet support single logout.
-
-Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
-
-====
-.Java
-[source,java,role="primary"]
-----
-http
-    // ...
-    .logout(logout -> logout
-        .logoutSuccessHandler(myCustomSuccessHandler())
-        .logoutRequestMatcher(myRequestMatcher())
-    )
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-http {
-    logout {
-        // ...
-        logoutSuccessHandler = myCustomSuccessHandler()
-        logoutRequestMatcher = myRequestMatcher()
-    }
-}
-----
-====
-
-The success handler will send logout requests to the asserting party.
-
-The request matcher will detect logout requests from the asserting party.
-

+ 29 - 384
docs/modules/ROOT/pages/servlet/saml2/saml2-login.adoc → docs/modules/ROOT/pages/servlet/saml2/login.adoc

@@ -1,7 +1,5 @@
-
-
 [[servlet-saml2login]]
 [[servlet-saml2login]]
-== SAML 2.0 Login
+= SAML 2.0 Login
 :figures: images/servlet/saml2
 :figures: images/servlet/saml2
 :icondir: images/icons
 :icondir: images/icons
 
 
@@ -69,21 +67,21 @@ image:{icondir}/number_4.png[] If authentication is successful, then __Success__
 * The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
 * The `Saml2WebSsoAuthenticationFilter` invokes `FilterChain#doFilter(request,response)` to continue with the rest of the application logic.
 
 
 [[servlet-saml2login-minimaldependencies]]
 [[servlet-saml2login-minimaldependencies]]
-=== Minimal Dependencies
+== Minimal Dependencies
 
 
 SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
 SAML 2.0 service provider support resides in `spring-security-saml2-service-provider`.
 It builds off of the OpenSAML library.
 It builds off of the OpenSAML library.
 
 
 [[servlet-saml2login-minimalconfiguration]]
 [[servlet-saml2login-minimalconfiguration]]
-=== Minimal Configuration
+== Minimal Configuration
 
 
 When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps.
 When using https://spring.io/projects/spring-boot[Spring Boot], configuring an application as a service provider consists of two basic steps.
 First, include the needed dependencies and second, indicate the necessary asserting party metadata.
 First, include the needed dependencies and second, indicate the necessary asserting party metadata.
 
 
 [NOTE]
 [NOTE]
-Also, this presupposes that you've already <<servlet-saml2login-metadata, registered the relying party with your asserting party>>.
+Also, this presupposes that you've already xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[registered the relying party with your asserting party].
 
 
-==== Specifying Identity Provider Metadata
+=== Specifying Identity Provider Metadata
 
 
 In a Spring Boot application, to specify an identity provider's metadata, simply do:
 In a Spring Boot application, to specify an identity provider's metadata, simply do:
 
 
@@ -116,7 +114,7 @@ And that's it!
 Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
 Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party.
 These are frequently abbreviated as AP and RP, respectively.
 These are frequently abbreviated as AP and RP, respectively.
 
 
-==== Runtime Expectations
+=== Runtime Expectations
 
 
 As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
 As configured above, the application processes any `+POST /login/saml2/sso/{registrationId}+` request containing a `SAMLResponse` parameter:
 
 
@@ -141,7 +139,7 @@ From here, consider jumping to:
 * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
 * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
 
 
 [[servlet-saml2login-architecture]]
 [[servlet-saml2login-architecture]]
-=== How SAML 2.0 Login Integrates with OpenSAML
+== How SAML 2.0 Login Integrates with OpenSAML
 
 
 Spring Security's SAML 2.0 support has a couple of design goals:
 Spring Security's SAML 2.0 support has a couple of design goals:
 
 
@@ -201,7 +199,7 @@ The resulting `Authentication#getPrincipal` is a Spring Security `Saml2Authentic
 `Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
 `Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId` holds the <<servlet-saml2login-relyingpartyregistrationid,identifier to the associated `RelyingPartyRegistration`>>.
 
 
 [[servlet-saml2login-opensaml-customization]]
 [[servlet-saml2login-opensaml-customization]]
-==== Customizing OpenSAML Configuration
+=== Customizing OpenSAML Configuration
 
 
 Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
 Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
 
 
@@ -293,7 +291,7 @@ companion object {
 The `requireInitialize` method may only be called once per application instance.
 The `requireInitialize` method may only be called once per application instance.
 
 
 [[servlet-saml2login-sansboot]]
 [[servlet-saml2login-sansboot]]
-=== Overriding or Replacing Boot Auto Configuration
+== Overriding or Replacing Boot Auto Configuration
 
 
 There are two ``@Bean``s that Spring Boot generates for a relying party.
 There are two ``@Bean``s that Spring Boot generates for a relying party.
 
 
@@ -524,7 +522,7 @@ class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
 A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
 A relying party can be multi-tenant by registering more than one relying party in the `RelyingPartyRegistrationRepository`.
 
 
 [[servlet-saml2login-relyingpartyregistration]]
 [[servlet-saml2login-relyingpartyregistration]]
-=== RelyingPartyRegistration
+== RelyingPartyRegistration
 A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
 A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[`RelyingPartyRegistration`]
 instance represents a link between an relying party and assering party's metadata.
 instance represents a link between an relying party and assering party's metadata.
 
 
@@ -618,7 +616,7 @@ The default for the `assertionConsumerServiceLocation` is `+/login/saml2/sso/{re
 It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
 It's mapped by default to <<servlet-saml2login-authentication-saml2webssoauthenticationfilter,`Saml2WebSsoAuthenticationFilter`>> in the filter chain.
 
 
 [[servlet-saml2login-rpr-uripatterns]]
 [[servlet-saml2login-rpr-uripatterns]]
-==== URI Patterns
+=== URI Patterns
 
 
 You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
 You probably noticed in the above examples the `+{baseUrl}+` and `+{registrationId}+` placeholders.
 
 
@@ -647,7 +645,7 @@ which in a deployed application would translate to
 `+https://rp.example.com/adfs+`
 `+https://rp.example.com/adfs+`
 
 
 [[servlet-saml2login-rpr-credentials]]
 [[servlet-saml2login-rpr-credentials]]
-==== Credentials
+=== Credentials
 
 
 You also likely noticed the credential that was used.
 You also likely noticed the credential that was used.
 
 
@@ -719,7 +717,7 @@ resource.inputStream.use {
 When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
 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]]
 [[servlet-saml2login-rpr-relyingpartyregistrationresolver]]
-==== Resolving the Relying Party from the Request
+=== 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.
 As seen so far, Spring Security resolves the `RelyingPartyRegistration` by looking for the registration id in the URI path.
 
 
@@ -763,13 +761,13 @@ class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationR
 ----
 ----
 ====
 ====
 
 
-Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce ``<saml2:AuthnRequest>``s>>, <<servlet-saml2login-authenticate-responses, authenticate ``<saml2:Response>``s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` metadata>>.
+Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce ``<saml2:AuthnRequest>``s>>, <<servlet-saml2login-authenticate-responses, authenticate ``<saml2:Response>``s>>, and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
 
 
 [NOTE]
 [NOTE]
 Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
 Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
 
 
 [[servlet-saml2login-rpr-duplicated]]
 [[servlet-saml2login-rpr-duplicated]]
-==== Duplicated Relying Party Configurations
+=== Duplicated Relying Party Configurations
 
 
 When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
 When an application uses multiple asserting parties, some configuration is duplicated between `RelyingPartyRegistration` instances:
 
 
@@ -864,7 +862,7 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
 ====
 ====
 
 
 [[servlet-saml2login-sp-initiated-factory]]
 [[servlet-saml2login-sp-initiated-factory]]
-=== Producing ``<saml2:AuthnRequest>``s
+== Producing ``<saml2:AuthnRequest>``s
 
 
 As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
 As stated earlier, Spring Security's SAML 2.0 support produces a `<saml2:AuthnRequest>` to commence authentication with the asserting party.
 
 
@@ -878,7 +876,7 @@ For example, if you were deployed to `https://rp.example.com` and you gave your
 and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
 and the result would be a redirect that included a `SAMLRequest` parameter containing the signed, deflated, and encoded `<saml2:AuthnRequest>`.
 
 
 [[servlet-saml2login-store-authn-request]]
 [[servlet-saml2login-store-authn-request]]
-==== Changing How the `<saml2:AuthnRequest>` Gets Stored
+=== Changing How the `<saml2:AuthnRequest>` Gets Stored
 
 
 `Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <<servlet-saml2login-sp-initiated-factory,sending the `<saml2:AuthnRequest>`>> to the asserting party.
 `Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <<servlet-saml2login-sp-initiated-factory,sending the `<saml2:AuthnRequest>`>> to the asserting party.
 
 
@@ -909,7 +907,7 @@ open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository
 ====
 ====
 
 
 [[servlet-saml2login-sp-initiated-factory-signing]]
 [[servlet-saml2login-sp-initiated-factory-signing]]
-==== Changing How the `<saml2:AuthnRequest>` Gets Sent
+=== Changing How the `<saml2:AuthnRequest>` Gets Sent
 
 
 By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
 By default, Spring Security signs each `<saml2:AuthnRequest>` and send it as a GET to the asserting party.
 
 
@@ -1036,7 +1034,7 @@ var relyingPartyRegistration: RelyingPartyRegistration? =
 ====
 ====
 
 
 [[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
 [[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
-==== Customizing OpenSAML's `AuthnRequest` Instance
+=== Customizing OpenSAML's `AuthnRequest` Instance
 
 
 There are a number of reasons that you may want to adjust an `AuthnRequest`.
 There are a number of reasons that you may want to adjust an `AuthnRequest`.
 For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
 For example, you may want `ForceAuthN` to be set to `true`, which Spring Security sets to `false` by default.
@@ -1157,7 +1155,7 @@ open fun authenticationRequestFactory(
 ====
 ====
 
 
 [[servlet-saml2login-authenticate-responses]]
 [[servlet-saml2login-authenticate-responses]]
-=== Authenticating ``<saml2:Response>``s
+== Authenticating ``<saml2:Response>``s
 
 
 To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
 To verify SAML 2.0 Responses, Spring Security uses <<servlet-saml2login-architecture,`OpenSaml4AuthenticationProvider`>> by default.
 
 
@@ -1171,7 +1169,7 @@ You can configure this in a number of ways including:
 To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
 To configure these, you'll use the `saml2Login#authenticationManager` method in the DSL.
 
 
 [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
 [[servlet-saml2login-opensamlauthenticationprovider-clockskew]]
-==== Setting a Clock Skew
+=== Setting a Clock Skew
 
 
 It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
 It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
 For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
 For that reason, you can configure `OpenSaml4AuthenticationProvider` 's default assertion validator with some tolerance:
@@ -1236,7 +1234,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() {
 ====
 ====
 
 
 [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
 [[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
-==== Coordinating with a `UserDetailsService`
+=== Coordinating with a `UserDetailsService`
 
 
 Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
 Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
 In that case, the response authentication converter can come in handy, as can be seen below:
 In that case, the response authentication converter can come in handy, as can be seen below:
@@ -1314,7 +1312,7 @@ It's not required to call `OpenSaml4AuthenticationProvider` 's default authentic
 It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority.
 It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority.
 
 
 [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
 [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]]
-==== Performing Additional Response Validation
+=== Performing Additional Response Validation
 
 
 `OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
 `OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`.
 You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
 You can customize the validation by extending the default validator concatenating with your own response validator, or you can replace it entirely with yours.
@@ -1336,7 +1334,7 @@ provider.setResponseValidator((responseToken) -> {
 });
 });
 ----
 ----
 
 
-==== Performing Additional Assertion Validation
+=== Performing Additional Assertion Validation
 `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
 `OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions.
 After verifying the signature, it will:
 After verifying the signature, it will:
 
 
@@ -1362,7 +1360,7 @@ provider.setAssertionValidator(assertionToken -> {
     OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
     OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
     ValidationContext context = new ValidationContext();
     ValidationContext context = new ValidationContext();
     try {
     try {
-        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
+        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
             return result;
             return result;
         }
         }
     } catch (Exception e) {
     } catch (Exception e) {
@@ -1385,7 +1383,7 @@ provider.setAssertionValidator { assertionToken ->
     val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
     val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
     val context = ValidationContext()
     val context = ValidationContext()
     try {
     try {
-        if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
+        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
             return@setAssertionValidator result
             return@setAssertionValidator result
         }
         }
     } catch (e: Exception) {
     } catch (e: Exception) {
@@ -1401,7 +1399,7 @@ While recommended, it's not necessary to call `OpenSaml4AuthenticationProvider`
 A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
 A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
 
 
 [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
 [[servlet-saml2login-opensamlauthenticationprovider-decryption]]
-==== Customizing Decryption
+=== Customizing Decryption
 
 
 Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
 Spring Security decrypts `<saml2:EncryptedAssertion>`, `<saml2:EncryptedAttribute>`, and `<saml2:EncryptedID>` elements automatically by using the decryption <<servlet-saml2login-rpr-credentials,`Saml2X509Credential` instances>> registered in the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>.
 
 
@@ -1451,7 +1449,7 @@ Trying to decrypt a signed assertion's elements before signature verification ma
 If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
 If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
 
 
 [[servlet-saml2login-authenticationmanager-custom]]
 [[servlet-saml2login-authenticationmanager-custom]]
-==== Using a Custom Authentication Manager
+=== Using a Custom Authentication Manager
 
 
 [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
 [[servlet-saml2login-opensamlauthenticationprovider-authenticationmanager]]
 Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
 Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
@@ -1500,7 +1498,7 @@ open class SecurityConfig : WebSecurityConfigurerAdapter() {
 ====
 ====
 
 
 [[servlet-saml2login-authenticatedprincipal]]
 [[servlet-saml2login-authenticatedprincipal]]
-=== Using `Saml2AuthenticatedPrincipal`
+== Using `Saml2AuthenticatedPrincipal`
 
 
 With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
 With the relying party correctly configured for a given asserting party, it's ready to accept assertions.
 Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
 Once the relying party validates an assertion, the result is a `Saml2Authentication` with a `Saml2AuthenticatedPrincipal`.
@@ -1540,356 +1538,3 @@ class MainController {
 [TIP]
 [TIP]
 Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
 Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
 `getFirstAttribute` is quite handy when you know that there is only one value.
 `getFirstAttribute` is quite handy when you know that there is only one value.
-
-[[servlet-saml2login-metadata]]
-=== Producing `<saml2:SPSSODescriptor>` Metadata
-
-You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
-
-====
-.Java
-[source,java,role="primary"]
-----
-DefaultRelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
-        new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
-Saml2MetadataFilter filter = new Saml2MetadataFilter(
-        relyingPartyRegistrationResolver,
-        new OpenSamlMetadataResolver());
-
-http
-    // ...
-    .saml2Login(withDefaults())
-    .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
-    DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
-val filter = Saml2MetadataFilter(
-    relyingPartyRegistrationResolver,
-    OpenSamlMetadataResolver()
-)
-
-http {
-    //...
-    saml2Login { }
-    addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
-}
-----
-====
-
-You can use this metadata endpoint to register your relying party with your asserting party.
-This is often as simple as finding the correct form field to supply the metadata endpoint.
-
-By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
-You can change this by calling the `setRequestMatcher` method on the filter:
-
-====
-.Java
-[source,java,role="primary"]
-----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
-----
-====
-
-Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
-----
-====
-
-[[servlet-saml2login-logout]]
-=== Performing Single Logout
-
-Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
-
-Briefly, there are two use cases Spring Security supports:
-
-* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
-Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
-* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
-Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
-
-[NOTE]
-In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
-Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
-
-=== Minimal Configuration for Single Logout
-
-To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
-
-* First, the asserting party must support SAML 2.0 Single Logout
-* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
-* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
-
-You can begin from the initial minimal example and add the following configuration:
-
-[source,java]
-----
-@Value("${private.key}") RSAPrivateKey key;
-@Value("${public.certificate}") X509Certificate certificate;
-
-@Bean
-RelyingPartyRegistrationRepository registrations() {
-    Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
-    RelyingPartyRegistration registration = RelyingPartyRegistrations
-            .fromMetadataLocation("https://ap.example.org/metadata")
-            .registrationId("id")
-            .signingX509Credentials((signing) -> signing.add(credential)) <1>
-            .build();
-    return new InMemoryRelyingPartyRegistrationRepository(registration);
-}
-
-@Bean
-SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
-    http
-        .authorizeRequests((authorize) -> authorize
-            .anyRequest().authenticated()
-        )
-        .saml2Login(withDefaults())
-        .saml2Logout(withDefaults()); <2>
-
-    return http.build();
-}
-----
-<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
-<2> - Second, indicate that your application wants to use SAML SLO to logout the end user
-
-==== Runtime Expectations
-
-Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
-Your application will then do the following:
-
-1. Logout the user and invalidate the session
-2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the currently logged-in user.
-3. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
-4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
-5. Redirect to any configured successful logout endpoint
-
-Also, your application can participate in an AP-initiated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
-
-1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
-2. Logout the user and invalidate the session
-3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
-4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
-
-=== Configuring Logout Endpoints
-
-There are three behaviors that can be triggered by different endpoints:
-
-* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `<saml2:LogoutRequest>`
-* AP-initiated logout, which allows an asserting party to send a `<saml2:LogoutRequest>` to the application
-* AP logout response, which allows an asserting party to send a `<saml2:LogoutResponse>` in response to the RP-initiated `<saml2:LogoutRequest>`
-
-The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`.
-
-The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party.
-
-The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party.
-
-Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known.
-For this reason, `+{registrationId}+` is not part of these URLs by default.
-
-This URL is customizable in the DSL.
-
-For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
-To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
-
-====
-.Java
-[source,java,role="primary"]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
-        .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
-    );
-----
-====
-
-You should also configure these endpoints in your `RelyingPartyRegistration`.
-
-=== Customizing `<saml2:LogoutRequest>` Resolution
-
-It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
-
-By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
-
-* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
-* The `ID` attribute - a GUID
-* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
-* The `<NameID>` element - from `Authentication#getName`
-
-To add other values, you can use delegation, like so:
-
-[source,java]
-----
-@Bean
-Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) {
-	OpenSaml4LogoutRequestResolver logoutRequestResolver
-			new OpenSaml4LogoutRequestResolver(registrationResolver);
-	logoutRequestResolver.setParametersConsumer((parameters) -> {
-		String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
-		String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
-		LogoutRequest logoutRequest = parameters.getLogoutRequest();
-		NameID nameId = logoutRequest.getNameID();
-		nameId.setValue(name);
-		nameId.setFormat(format);
-	});
-	return logoutRequestResolver;
-}
-----
-
-Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows:
-
-[source,java]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutRequest((request) -> request
-            .logoutRequestResolver(this.logoutRequestResolver)
-        )
-    );
-----
-
-=== Customizing `<saml2:LogoutResponse>` Resolution
-
-It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
-
-By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
-
-* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
-* The `ID` attribute - a GUID
-* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
-* The `<Status>` element - `SUCCESS`
-
-To add other values, you can use delegation, like so:
-
-[source,java]
-----
-@Bean
-public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) {
-	OpenSaml4LogoutResponseResolver logoutRequestResolver =
-			new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
-	logoutRequestResolver.setParametersConsumer((parameters) -> {
-		if (checkOtherPrevailingConditions(parameters.getRequest())) {
-			parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
-		}
-	});
-	return logoutRequestResolver;
-}
-----
-
-Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows:
-
-[source,java]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutRequest((request) -> request
-            .logoutRequestResolver(this.logoutRequestResolver)
-        )
-    );
-----
-
-=== Customizing `<saml2:LogoutRequest>` Authentication
-
-To customize validation, you can implement your own `Saml2LogoutRequestValidator`.
-At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so:
-
-[source,java]
-----
-@Component
-public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
-	private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();
-
-	@Override
-    public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
-		 // verify signature, issuer, destination, and principal name
-		Saml2LogoutValidatorResult result = delegate.authenticate(authentication);
-
-		LogoutRequest logoutRequest = // ... parse using OpenSAML
-        // perform custom validation
-    }
-}
-----
-
-Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows:
-
-[source,java]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutRequest((request) -> request
-            .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator)
-        )
-    );
-----
-
-=== Customizing `<saml2:LogoutResponse>` Authentication
-
-To customize validation, you can implement your own `Saml2LogoutResponseValidator`.
-At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so:
-
-[source,java]
-----
-@Component
-public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
-	private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();
-
-	@Override
-    public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
-		// verify signature, issuer, destination, and status
-		Saml2LogoutValidatorResult result = delegate.authenticate(parameters);
-
-		LogoutResponse logoutResponse = // ... parse using OpenSAML
-        // perform custom validation
-    }
-}
-----
-
-Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows:
-
-[source,java]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutResponse((response) -> response
-            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
-        )
-    );
-----
-
-=== Customizing `<saml2:LogoutRequest>` storage
-
-When your application sends a `<saml2:LogoutRequest>`, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `<saml2:LogoutResponse>` can be verified.
-
-If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so:
-
-[source,java]
-----
-http
-    .saml2Logout((saml2) -> saml2
-        .logoutRequest((request) -> request
-            .logoutRequestRepository(myCustomLogoutRequestRepository)
-        )
-    );
-----

+ 277 - 0
docs/modules/ROOT/pages/servlet/saml2/logout.adoc

@@ -0,0 +1,277 @@
+[[servlet-saml2login-logout]]
+= Performing Single Logout
+
+Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout.
+
+Briefly, there are two use cases Spring Security supports:
+
+* **RP-Initiated** - Your application has an endpoint that, when POSTed to, will logout the user and send a `saml2:LogoutRequest` to the asserting party.
+Thereafter, the asserting party will send back a `saml2:LogoutResponse` and allow your application to respond
+* **AP-Initiated** - Your application has an endpoint that will receive a `saml2:LogoutRequest` from the asserting party.
+Your application will complete its logout at that point and then send a `saml2:LogoutResponse` to the asserting party.
+
+[NOTE]
+In the **AP-Initiated** scenario, any local redirection that your application would do post-logout is rendered moot.
+Once your application sends a `saml2:LogoutResponse`, it no longer has control of the browser.
+
+== Minimal Configuration for Single Logout
+
+To use Spring Security's SAML 2.0 Single Logout feature, you will need the following things:
+
+* First, the asserting party must support SAML 2.0 Single Logout
+* Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint
+* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
+
+You can begin from the initial minimal example and add the following configuration:
+
+[source,java]
+----
+@Value("${private.key}") RSAPrivateKey key;
+@Value("${public.certificate}") X509Certificate certificate;
+
+@Bean
+RelyingPartyRegistrationRepository registrations() {
+    Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
+    RelyingPartyRegistration registration = RelyingPartyRegistrations
+            .fromMetadataLocation("https://ap.example.org/metadata")
+            .registrationId("id")
+            .signingX509Credentials((signing) -> signing.add(credential)) <1>
+            .build();
+    return new InMemoryRelyingPartyRegistrationRepository(registration);
+}
+
+@Bean
+SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
+    http
+        .authorizeRequests((authorize) -> authorize
+            .anyRequest().authenticated()
+        )
+        .saml2Login(withDefaults())
+        .saml2Logout(withDefaults()); <2>
+
+    return http.build();
+}
+----
+<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login.adoc#servlet-saml2login-rpr-duplicated[multiple instances]
+<2> - Second, indicate that your application wants to use SAML SLO to logout the end user
+
+=== Runtime Expectations
+
+Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
+Your application will then do the following:
+
+1. Logout the user and invalidate the session
+2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the currently logged-in user.
+3. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
+4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
+5. Redirect to any configured successful logout endpoint
+
+Also, your application can participate in an AP-initiated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
+
+1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
+2. Logout the user and invalidate the session
+3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] associated with the just logged-out user
+4. Send a redirect or post to the asserting party based on the xref:servlet/saml2/login.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
+
+== Configuring Logout Endpoints
+
+There are three behaviors that can be triggered by different endpoints:
+
+* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `<saml2:LogoutRequest>`
+* AP-initiated logout, which allows an asserting party to send a `<saml2:LogoutRequest>` to the application
+* AP logout response, which allows an asserting party to send a `<saml2:LogoutResponse>` in response to the RP-initiated `<saml2:LogoutRequest>`
+
+The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`.
+
+The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party.
+
+The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party.
+
+Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known.
+For this reason, `+{registrationId}+` is not part of these URLs by default.
+
+This URL is customizable in the DSL.
+
+For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
+To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
+        .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
+    );
+----
+====
+
+You should also configure these endpoints in your `RelyingPartyRegistration`.
+
+== Customizing `<saml2:LogoutRequest>` Resolution
+
+It's common to need to set other values in the `<saml2:LogoutRequest>` than the defaults that Spring Security provides.
+
+By default, Spring Security will issue a `<saml2:LogoutRequest>` and supply:
+
+* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation`
+* The `ID` attribute - a GUID
+* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
+* The `<NameID>` element - from `Authentication#getName`
+
+To add other values, you can use delegation, like so:
+
+[source,java]
+----
+@Bean
+Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) {
+	OpenSaml4LogoutRequestResolver logoutRequestResolver
+			new OpenSaml4LogoutRequestResolver(registrationResolver);
+	logoutRequestResolver.setParametersConsumer((parameters) -> {
+		String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
+		String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
+		LogoutRequest logoutRequest = parameters.getLogoutRequest();
+		NameID nameId = logoutRequest.getNameID();
+		nameId.setValue(name);
+		nameId.setFormat(format);
+	});
+	return logoutRequestResolver;
+}
+----
+
+Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestResolver(this.logoutRequestResolver)
+        )
+    );
+----
+
+== Customizing `<saml2:LogoutResponse>` Resolution
+
+It's common to need to set other values in the `<saml2:LogoutResponse>` than the defaults that Spring Security provides.
+
+By default, Spring Security will issue a `<saml2:LogoutResponse>` and supply:
+
+* The `Destination` attribute - from `RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation`
+* The `ID` attribute - a GUID
+* The `<Issuer>` element - from `RelyingPartyRegistration#getEntityId`
+* The `<Status>` element - `SUCCESS`
+
+To add other values, you can use delegation, like so:
+
+[source,java]
+----
+@Bean
+public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) {
+	OpenSaml4LogoutResponseResolver logoutRequestResolver =
+			new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
+	logoutRequestResolver.setParametersConsumer((parameters) -> {
+		if (checkOtherPrevailingConditions(parameters.getRequest())) {
+			parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
+		}
+	});
+	return logoutRequestResolver;
+}
+----
+
+Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestResolver(this.logoutRequestResolver)
+        )
+    );
+----
+
+== Customizing `<saml2:LogoutRequest>` Authentication
+
+To customize validation, you can implement your own `Saml2LogoutRequestValidator`.
+At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so:
+
+[source,java]
+----
+@Component
+public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
+	private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();
+
+	@Override
+    public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
+		 // verify signature, issuer, destination, and principal name
+		Saml2LogoutValidatorResult result = delegate.authenticate(authentication);
+
+		LogoutRequest logoutRequest = // ... parse using OpenSAML
+        // perform custom validation
+    }
+}
+----
+
+Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator)
+        )
+    );
+----
+
+== Customizing `<saml2:LogoutResponse>` Authentication
+
+To customize validation, you can implement your own `Saml2LogoutResponseValidator`.
+At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so:
+
+[source,java]
+----
+@Component
+public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
+	private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();
+
+	@Override
+    public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
+		// verify signature, issuer, destination, and status
+		Saml2LogoutValidatorResult result = delegate.authenticate(parameters);
+
+		LogoutResponse logoutResponse = // ... parse using OpenSAML
+        // perform custom validation
+    }
+}
+----
+
+Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutResponse((response) -> response
+            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
+        )
+    );
+----
+
+== Customizing `<saml2:LogoutRequest>` storage
+
+When your application sends a `<saml2:LogoutRequest>`, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `<saml2:LogoutResponse>` can be verified.
+
+If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so:
+
+[source,java]
+----
+http
+    .saml2Logout((saml2) -> saml2
+        .logoutRequest((request) -> request
+            .logoutRequestRepository(myCustomLogoutRequestRepository)
+        )
+    );
+----

+ 74 - 0
docs/modules/ROOT/pages/servlet/saml2/metadata.adoc

@@ -0,0 +1,74 @@
+[[servlet-saml2login-metadata]]
+= Producing `<saml2:SPSSODescriptor>` Metadata
+
+You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
+
+====
+.Java
+[source,java,role="primary"]
+----
+DefaultRelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
+        new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
+Saml2MetadataFilter filter = new Saml2MetadataFilter(
+        relyingPartyRegistrationResolver,
+        new OpenSamlMetadataResolver());
+
+http
+    // ...
+    .saml2Login(withDefaults())
+    .addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
+    DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
+val filter = Saml2MetadataFilter(
+    relyingPartyRegistrationResolver,
+    OpenSamlMetadataResolver()
+)
+
+http {
+    //...
+    saml2Login { }
+    addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
+}
+----
+====
+
+You can use this metadata endpoint to register your relying party with your asserting party.
+This is often as simple as finding the correct form field to supply the metadata endpoint.
+
+By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
+You can change this by calling the `setRequestMatcher` method on the filter:
+
+====
+.Java
+[source,java,role="primary"]
+----
+filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
+----
+====
+
+Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
+
+====
+.Java
+[source,java,role="primary"]
+----
+filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
+----
+====