Prechádzať zdrojové kódy

Separate SAML 2.0 Login Docs

Issue gh-10367
Josh Cummings 3 rokov pred
rodič
commit
11aa02c6fb

+ 4 - 1
docs/modules/ROOT/nav.adoc

@@ -63,7 +63,10 @@
 **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
 **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
 ** xref:servlet/saml2/index.adoc[SAML2]
-*** xref:servlet/saml2/login.adoc[SAML2 Log In]
+*** xref:servlet/saml2/login/index.adoc[SAML2 Log In]
+**** xref:servlet/saml2/login/overview.adoc[SAML2 Log In Overview]
+**** xref:servlet/saml2/login/authentication-requests.adoc[SAML2 Authentication Requests]
+**** xref:servlet/saml2/login/authentication.adoc[SAML2 Authentication Responses]
 *** xref:servlet/saml2/logout.adoc[SAML2 Logout]
 *** xref:servlet/saml2/metadata.adoc[SAML2 Metadata]
 ** xref:servlet/exploits/index.adoc[Protection Against Exploits]

+ 293 - 0
docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc

@@ -0,0 +1,293 @@
+[[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-store-authn-request]]
+== Changing How the `<saml2:AuthnRequest>` Gets Stored
+
+`Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[sending the `<saml2:AuthnRequest>`] to the asserting party.
+
+Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticating the `<saml2:Response>`].
+
+By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`.
+
+If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
+
+====
+.Java
+[source,java,role="primary"]
+----
+@Bean
+Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
+	return new CustomSaml2AuthenticationRequestRepository();
+}
+----
+
+.Kotlin
+[source,kotlin,role="secondary"]
+----
+@Bean
+open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
+    return CustomSaml2AuthenticationRequestRepository()
+}
+----
+====
+
+[[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 xref:servlet/saml2/login/overview.adoc#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 xref:servlet/saml2/login/overview.adoc#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
+}
+----
+====
+

+ 384 - 0
docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc

@@ -0,0 +1,384 @@
+[[servlet-saml2login-authenticate-responses]]
+= Authenticating ``<saml2:Response>``s
+
+To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#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 xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-credentials[`Saml2X509Credential` instances] registered in the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`].
+
+`OpenSaml4AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#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.

+ 18 - 0
docs/modules/ROOT/pages/servlet/saml2/login/index.adoc

@@ -0,0 +1,18 @@
+[[servlet-saml2login]]
+= SAML 2.0 Login
+:page-section-summary-toc: 1
+
+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].
+====

+ 9 - 703
docs/modules/ROOT/pages/servlet/saml2/login.adoc → docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc

@@ -1,22 +1,6 @@
-[[servlet-saml2login]]
-= SAML 2.0 Login
-:figures: images/servlet/saml2
-:icondir: images/icons
-
-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].
-====
+= SAML 2.0 Login Overview
+:figures: servlet/saml2
+:icondir: icons
 
 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[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication.
@@ -32,7 +16,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to
 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`.
+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 xref:servlet/saml2/login/authentication-requests.adoc#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`>>.
@@ -49,7 +33,7 @@ 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`>>.
+image:{icondir}/number_1.png[] When the browser submits a `<saml2:Response>` to the application, it xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[delegates to `Saml2WebSsoAuthenticationFilter`].
 This filter calls its configured `AuthenticationConverter` to create a `Saml2AuthenticationToken` by extracting the response from the `HttpServletRequest`.
 This converter additionally resolves the <<servlet-saml2login-relyingpartyregistration, `RelyingPartyRegistration`>> and supplies it to `Saml2AuthenticationToken`.
 
@@ -135,7 +119,7 @@ Your app then redirects to the configured asserting party which then sends the `
 From here, consider jumping to:
 
 * <<servlet-saml2login-architecture,How SAML 2.0 Login Integrates with OpenSAML>>
-* <<servlet-saml2login-authenticatedprincipal,How to Use the `Saml2AuthenticatedPrincipal`>>
+* xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticatedprincipal[How to Use the `Saml2AuthenticatedPrincipal`]
 * <<servlet-saml2login-sansboot,How to Override or Replace Spring Boot's Auto Configuration>>
 
 [[servlet-saml2login-architecture]]
@@ -172,7 +156,7 @@ image:{icondir}/number_2.png[] The xref:servlet/authentication/architecture.adoc
 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>>.
+image:{icondir}/number_4.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#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.
@@ -183,7 +167,7 @@ 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>>.
+image:{icondir}/number_7.png[] Then, the provider xref:servlet/saml2/login/authentication.adoc#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.
@@ -761,7 +745,7 @@ 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 xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
+Then, you can provide this resolver to the appropriate filters that xref:servlet/saml2/login/authentication-requests.adoc#servlet-saml2login-sp-initiated-factory[produce ``<saml2:AuthnRequest>``s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ``<saml2:Response>``s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `<saml2:SPSSODescriptor>` metadata].
 
 [NOTE]
 Remember that if you have any placeholders in your `RelyingPartyRegistration`, your resolver implementation should resolve them.
@@ -860,681 +844,3 @@ open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
 }
 ----
 ====
-
-[[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-store-authn-request]]
-=== 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.
-
-Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of <<servlet-saml2login-authenticate-responses,authenticating the `<saml2:Response>`>>.
-
-By default, Spring Security uses an `HttpSessionSaml2AuthenticationRequestRepository`, which stores the `AbstractSaml2AuthenticationRequest` in the `HttpSession`.
-
-If you have a custom implementation of `Saml2AuthenticationRequestRepository`, you may configure it by exposing it as a `@Bean` as shown in the following example:
-
-====
-.Java
-[source,java,role="primary"]
-----
-@Bean
-Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> authenticationRequestRepository() {
-	return new CustomSaml2AuthenticationRequestRepository();
-}
-----
-
-.Kotlin
-[source,kotlin,role="secondary"]
-----
-@Bean
-open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
-    return CustomSaml2AuthenticationRequestRepository()
-}
-----
-====
-
-[[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.

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

@@ -52,7 +52,7 @@ SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository re
     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]
+<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login/overview.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
@@ -61,8 +61,8 @@ Given the above configuration any logged in user can send a `POST /logout` to yo
 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`]
+2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `<saml2:LogoutRequest>` based on the xref:servlet/saml2/login/overview.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/overview.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
 
@@ -70,8 +70,8 @@ Also, your application can participate in an AP-initiated logout when the assert
 
 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`]
+3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the xref:servlet/saml2/login/overview.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/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`]
 
 == Configuring Logout Endpoints