Josh Cummings 9b724377ce Rework Saml2 Authentication Statement
This commit separates the authentication principal, the assertion details,
and the relying party tenant into separate components. This allows the
principal to be completely decoupled from how Spring Security triggers and
processes SLO.

Specifically, it adds Saml2AssertionAuthentication, a new authentication
implementation that allows an Object principal and a Saml2ResponseAssertionAccessor
credential. It also moves the relying party registration id from
Saml2AuthenticatedPrincipal to Saml2AssertionAuthentication.

As such, Saml2AuthenticatedPrincipal is now deprecated in favor of
placing its assertion components in Saml2ResponseAssertionAccessor and
the relying party registration id in Saml2AssertionAuthentication.

Closes gh-10820
2025-06-10 17:21:03 -06:00

111 lines
4.0 KiB
Plaintext

= SAML 2.0 Migrations
== Expect `<saml2:LogoutResponse>` When `<saml2:LogoutRequest>` Validation Fails
SAML identity providers expect service providers to return an error `<saml2:LogoutResponse>` if it fails to process the `<saml2:LogoutRequest>`.
Past versions of Spring Security returned a 401 in some cases, breaking the chain of logout requests and responses from each relying party.
In Spring Security 7, this behavior is repaired, and you need do nothing.
However, if this gives you trouble, you can revert back to the old behavior by publishing a `Saml2LogoutRequestResolver` that returns `null` when an error `<saml2:LogoutRequest>` is needed.
You can create a delegate like this one:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
OpenSaml5LogoutResponseResolver delegate = new OpenSaml5LogoutResponseResolver(registrations);
return new Saml2LogoutResponseResolver() {
@Override
public void resolve(HttpServletRequest request, Authentication authentication) {
delegate.resolve(request, authentication);
}
@Override
public void resolve(HttpServletRequest request, Authentication authentication, Saml2AuthenticationException error) {
return null;
}
};
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?): Saml2LogoutResponseResolver {
val delegate = OpenSaml5LogoutResponseResolver(registrations)
return object : Saml2LogoutResponseResolver() {
override fun resolve(request: HttpServletRequest?, authentication: Authentication?) {
delegate.resolve(request, authentication)
}
override fun resolve(request: HttpServletRequest?, authentication: Authentication?, error: Saml2AuthenticationException?) {
return null
}
}
}
----
======
== Favor `Saml2ResponseAuthenticationAccessor` over `Saml2AuthenticatedPrincipal`
Spring Security 7 separates `<saml2:Assertion>` details from the principal.
This allows Spring Security to retrieve needed assertion details to perform Single Logout.
This deprecates `Saml2AuthenticatedPrincipal`.
You no longer need to implement it to use `Saml2Authentication`.
Instead, the credential implements `Saml2ResponseAssertionAccessor`, which Spring Security 7 favors when determining the appropriate action based on the authentication.
This change is made automatically for you when using the defaults.
If this causes you trouble when upgrading, you can publish a custom `ResponseAuhenticationConverter` to return a `Saml2Authentication` instead of returning a `Saml2AssertionAuthentication` like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
OpenSaml5AuthenticationProvider authenticationProvider() {
OpenSaml5AuthenticationProvider authenticationProvider =
new OpenSaml5AuthenticationProvider();
ResponseAuthenticationConverter defaults = new ResponseAuthenticationConverter();
authenticationProvider.setResponseAuthenticationConverter(
defaults.andThen((authentication) -> new Saml2Authentication(
authentication.getPrincipal(),
authentication.getSaml2Response(),
authentication.getAuthorities())));
return authenticationProvider;
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authenticationProvider(): OpenSaml5AuthenticationProvider {
val authenticationProvider = OpenSaml5AuthenticationProvider()
val defaults = ResponseAuthenticationConverter()
authenticationProvider.setResponseAuthenticationConverter(
defaults.andThen { authentication ->
Saml2Authentication(authentication.getPrincipal(),
authentication.getSaml2Response(),
authentication.getAuthorities())
})
return authenticationProvider
}
----
======
If you are constructing a `Saml2Authentication` instance yourself, consider changing to `Saml2AssertionAuthentication` to get the same benefit as the current default.