From c6bba384580303700bf641dbd64aafa8412a5ce2 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Tue, 6 May 2025 16:38:32 -0600 Subject: [PATCH] Update SAML 2.0 Migration Steps --- .../modules/ROOT/pages/migration-7/saml2.adoc | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/docs/modules/ROOT/pages/migration-7/saml2.adoc b/docs/modules/ROOT/pages/migration-7/saml2.adoc index eed236a64b..43da1be2b0 100644 --- a/docs/modules/ROOT/pages/migration-7/saml2.adoc +++ b/docs/modules/ROOT/pages/migration-7/saml2.adoc @@ -1,5 +1,17 @@ = Saml 2.0 Migrations +== Use OpenSAML 5 By Default + +OpenSAML 4.x is no longer supported by the OpenSAML team. +As such, Spring Security will default to using its `OpenSaml5` components in all cases. + +If you want to see how well your application will respond to this, do the following: + +1. Update your OpenSAML dependencies to 5.x +2. If you are constructing an `OpenSaml4XXX` Spring Security component, change it to `OpenSaml5`. + +If you cannot opt-in, then add the `opensaml-saml-api` and `opensaml-saml-impl` 4.x dependencies and exclude the 5.x dependencies from `spring-security-saml2-service-provider`. + == Continue Filter Chain When No Relying Party Found In Spring Security 6, `Saml2WebSsoAuthenticationFilter` throws an exception when the request URI matches, but no relying party registration is found. @@ -163,3 +175,230 @@ val responseValidator = ResponseValidator.withDefaults { responseToken: Response provider.setResponseValidator(responseValidator) ---- ====== + +== `RelyingPartyRegistration` Improvements + +`RelyingPartyRegistration` links metadata from a relying party to metadata from an asserting party. + +To prepare for some improvements to the API, please take the following steps: + +1. If you are mutating a registration by using `RelyingPartyRegistration#withRelyingPartyRegistration`, instead call `RelyingPartyRegistration#mutate` +2. If you are providing or retrieving `AssertingPartyDetails`, use `getAssertingPartyMetadata` or `withAssertingPartyMetadata` instead. + +== `OpenSaml5AuthenticationProvider` Improvements + +Spring Security 7 will remove a handful of static factories from `OpenSaml5AuthenticationProvider` in favor of inner classes. +These inner classes simplify customization of the response validator, the assertion validator, and the response authentication converter. + +=== Response Validation + +Instead of doing: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { + OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); + saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator() + .andThen((result) -> result + .concat(myCustomValidator.convert(responseToken)) + )); + return saml2; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { + val saml2 = OpenSaml5AuthenticationProvider() + saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator() + .andThen { result -> result + .concat(myCustomValidator.convert(responseToken)) + } + } + return saml2 +} +---- +====== + +use `OpenSaml5AuthenticationProvider.ResponseValidator`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { + OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); + saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator)); + return saml2; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { + val saml2 = OpenSaml5AuthenticationProvider() + saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator)) + return saml2 +} +---- +====== + +=== Assertion Validation + +Instead of doing: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { + OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); + authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider + .createDefaultAssertionValidatorWithParameters(assertionToken -> { + Map params = new HashMap<>(); + params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()); + // ... other validation parameters + return new ValidationContext(params); + }) + ); + return saml2; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { + val saml2 = OpenSaml5AuthenticationProvider() + authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider + .createDefaultAssertionValidatorWithParameters { -> + val params = HashMap() + params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()) + // ... other validation parameters + return ValidationContext(params) + } + ) + return saml2 +} +---- +====== + +use `OpenSaml5AuthenticationProvider.AssertionValidator`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +OpenSaml5AuthenticationProvider saml2AuthenticationProvider() { + OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider(); + Duration tenMinutes = Duration.ofMinutes(10); + authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build()); + return saml2; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider { + val saml2 = OpenSaml5AuthenticationProvider() + val tenMinutes = Duration.ofMinutes(10) + authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build()) + return saml2 +} +---- +====== + +== Response Authentication Converter + +Instead of doing: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +Converter authenticationConverter() { + return (responseToken) -> { + Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter() + .convert(responseToken); + // ... work with OpenSAML's Assertion object to extract the principal + return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities()); + }; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun authenticationConverter(): Converter { + return { responseToken -> + val authentication = + OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken) + // ... work with OpenSAML's Assertion object to extract the principal + return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities()) + } +} +---- +====== + +use `OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter`: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +ResponseAuthenticationConverter authenticationConverter() { + ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter(); + authenticationConverter.setPrincipalNameConverter((assertion) -> { + // ... work with OpenSAML's Assertion object to extract the principal + }); + return authenticationConverter; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun authenticationConverter(): ResponseAuthenticationConverter { + val authenticationConverter = ResponseAuthenticationConverter() + authenticationConverter.setPrincipalNameConverter { assertion -> + // ... work with OpenSAML's Assertion object to extract the principal + } + return authenticationConverter +} +---- +======