diff --git a/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc b/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc index 293abadddb..1d6690f0fc 100644 --- a/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc +++ b/docs/modules/ROOT/pages/migration/servlet/oauth2.adoc @@ -83,7 +83,7 @@ fun jwtDecoder(): JwtDecoder { Spring Security does not support processing `` payloads over GET as this is not supported by the SAML 2.0 spec. -To better comply with this, `Saml2AuthenticationTokenConverter`, `OpenSaml4AuthenticationTokenConverter`, and `OpenSaml5AuthenticationTokenConverter` will not process GET requests by default as of Spring Security 8. +To better comply with this, `Saml2AuthenticationTokenConverter` and `OpenSaml5AuthenticationTokenConverter` will not process GET requests by default as of Spring Security 8. To prepare for this, the property `shouldConvertGetRequests` is available. To use it, publish your own converter like so: @@ -114,7 +114,7 @@ fun authenticationConverter(val registrations: RelyingPartyRegistrationRepositor ---- ====== -If you must continue using `Saml2AuthenticationTokenConverter`, `OpenSaml4AuthenticationTokenConverter`, or `OpenSaml5AuthenticationTokenConverter` to process GET requests, you can call `setShouldConvertGetRequests` to `true.` +If you must continue using `Saml2AuthenticationTokenConverter` or `OpenSaml5AuthenticationTokenConverter` to process GET requests, you can call `setShouldConvertGetRequests` to `true.` == Provide an AuthenticationConverter to BearerTokenAuthenticationFilter diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc index ccce691c04..3e79d74c83 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -267,7 +267,7 @@ var relyingPartyRegistration: RelyingPartyRegistration? = 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. -You can customize elements of OpenSAML's `AuthnRequest` by publishing an `OpenSaml4AuthenticationRequestResolver` as a `@Bean`, like so: +You can customize elements of OpenSAML's `AuthnRequest` by publishing an `OpenSaml5AuthenticationRequestResolver` as a `@Bean`, like so: [tabs] ====== @@ -279,8 +279,8 @@ Java:: Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationRepository registrations) { RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations); - OpenSaml4AuthenticationRequestResolver authenticationRequestResolver = - new OpenSaml4AuthenticationRequestResolver(registrationResolver); + OpenSaml5AuthenticationRequestResolver authenticationRequestResolver = + new OpenSaml5AuthenticationRequestResolver(registrationResolver); authenticationRequestResolver.setAuthnRequestCustomizer((context) -> context .getAuthnRequest().setForceAuthn(true)); return authenticationRequestResolver; @@ -295,8 +295,8 @@ Kotlin:: fun authenticationRequestResolver(registrations : RelyingPartyRegistrationRepository) : Saml2AuthenticationRequestResolver { val registrationResolver : RelyingPartyRegistrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations) - val authenticationRequestResolver : OpenSaml4AuthenticationRequestResolver = - new OpenSaml4AuthenticationRequestResolver(registrationResolver) + val authenticationRequestResolver : OpenSaml5AuthenticationRequestResolver = + new OpenSaml5AuthenticationRequestResolver(registrationResolver) authenticationRequestResolver.setAuthnRequestCustomizer((context) -> context .getAuthnRequest().setForceAuthn(true)) return authenticationRequestResolver diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc index 1137a59276..632c87995d 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -1,7 +1,7 @@ [[servlet-saml2login-authenticate-responses]] = Authenticating ````s -To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml4AuthenticationProvider`] to authenticate it. +To verify SAML 2.0 Responses, Spring Security uses xref:servlet/saml2/login/overview.adoc#servlet-saml2login-authentication-saml2authenticationtokenconverter[`Saml2AuthenticationTokenConverter`] to populate the `Authentication` request and xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[`OpenSaml5AuthenticationProvider`] to authenticate it. You can configure this in a number of ways including: @@ -123,76 +123,7 @@ fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConvert == 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: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); - authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider - .createDefaultAssertionValidatorWithParameters(assertionToken -> { - Map params = new HashMap<>(); - params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis()); - // ... other validation parameters - return new ValidationContext(params); - }) - ); - - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .saml2Login((saml2) -> saml2 - .authenticationManager(new ProviderManager(authenticationProvider)) - ); - return http.build(); - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebSecurity -open class SecurityConfig { - @Bean - open fun filterChain(http: HttpSecurity): SecurityFilterChain { - val authenticationProvider = OpenSaml4AuthenticationProvider() - authenticationProvider.setAssertionValidator( - OpenSaml4AuthenticationProvider - .createDefaultAssertionValidatorWithParameters(Converter { - val params: MutableMap = HashMap() - params[CLOCK_SKEW] = - Duration.ofMinutes(10).toMillis() - ValidationContext(params) - }) - ) - http { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - saml2Login { - authenticationManager = ProviderManager(authenticationProvider) - } - } - return http.build() - } -} ----- -====== - -If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way, using `OpenSaml5AuthenticationProvider.AssertionValidator`: +For that reason, you can configure `OpenSaml5AuthenticationProvider.AssertionValidator` as follows: [tabs] ====== @@ -381,86 +312,8 @@ open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAu If your `UserDetailsService` returns a value that also implements `AuthenticatedPrincipal`, then you don't need a custom authentication implementation. ==== -Or, if you are using OpenSaml 4, then you can achieve something similar as follows: - -[tabs] -====== -Java:: -+ -[source,java,role="primary"] ----- -@Configuration -@EnableWebSecurity -public class SecurityConfig { - @Autowired - UserDetailsService userDetailsService; - - @Bean - public SecurityFilterChain filterChain(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 - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .saml2Login((saml2) -> saml2 - .authenticationManager(new ProviderManager(authenticationProvider)) - ); - return http.build(); - } -} ----- - -Kotlin:: -+ -[source,kotlin,role="secondary"] ----- -@Configuration -@EnableWebSecurity -open class SecurityConfig { - @Autowired - var userDetailsService: UserDetailsService? = null - - @Bean - open fun filterChain(http: HttpSecurity): SecurityFilterChain { - 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 { - authorizeHttpRequests { - authorize(anyRequest, authenticated) - } - saml2Login { - authenticationManager = ProviderManager(authenticationProvider) - } - } - return http.build() - } -} ----- -====== -<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's not required to call ``OpenSaml5AuthenticationProvider``'s default authentication converter. It returns a `Saml2AuthenticatedPrincipal` containing the attributes it extracted from ``AttributeStatement``s as well as the single `ROLE_USER` authority. === Configuring the Principal Name @@ -538,28 +391,10 @@ fun authenticationConverter(): ResponseAuthenticationConverter { [[servlet-saml2login-opensamlauthenticationprovider-additionalvalidation]] == Performing Additional Response Validation -`OpenSaml4AuthenticationProvider` validates the `Issuer` and `Destination` values right after decrypting the `Response`. +`OpenSaml5AuthenticationProvider` 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 = OpenSaml4AuthenticationProvider - .createDefaultResponseValidator() - .convert(responseToken) - .concat(myCustomValidator.convert(responseToken)); - if (!result.getErrors().isEmpty()) { - String inResponseTo = responseToken.getInResponseTo(); - throw new CustomSaml2AuthenticationException(result, inResponseTo); - } - return result; -}); ----- - -When using `OpenSaml5AuthenticationProvider`, you can do the same with less boilerplate: - [source,java] ---- OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider(); @@ -583,74 +418,17 @@ OpenSAML performs `Asssertion#InResponseTo` validation in its `BearerSubjectConf ==== == Performing Additional Assertion Validation -`OpenSaml4AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions. +`OpenSaml5AuthenticationProvider` performs minimal validation on SAML 2.0 Assertions. After verifying the signature, it will: 1. Validate `` and `` conditions 2. Validate ````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. +To perform additional validation, you can configure your own assertion validator that delegates to ``OpenSaml5AuthenticationProvider``'s default and then performs its own. [[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]] For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `` condition, like so: -[tabs] -====== -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 `` or the `` since you are doing those yourself. - -If you are using xref:servlet/saml2/opensaml.adoc[OpenSAML 5], then we have a simpler way using `OpenSaml5AuthenticationProvider.AssertionValidator`: - [tabs] ====== Java:: @@ -708,11 +486,11 @@ provider.setAssertionValidator(assertionValidator) Spring Security decrypts ``, ``, and `` 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]. +`OpenSaml5AuthenticationProvider` exposes xref:servlet/saml2/login/overview.adoc#servlet-saml2login-architecture[two decryption strategies]. The response decrypter is for decrypting encrypted elements of the ``, like ``. The assertion decrypter is for decrypting encrypted elements of the ``, like `` and ``. -You can replace ``OpenSaml4AuthenticationProvider``'s default decryption strategy with your own. +You can replace ``OpenSaml5AuthenticationProvider``'s default decryption strategy with your own. For example, if you have a separate service that decrypts the assertions in a ``, you can use it instead like so: [tabs] @@ -722,7 +500,7 @@ Java:: [source,java,role="primary"] ---- MyDecryptionService decryptionService = ...; -OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider(); +OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider(); provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse())); ---- @@ -731,7 +509,7 @@ Kotlin:: [source,kotlin,role="secondary"] ---- val decryptionService: MyDecryptionService = ... -val provider = OpenSaml4AuthenticationProvider() +val provider = OpenSaml5AuthenticationProvider() provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) } ---- ====== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index 2c1974a920..c1b46ce3e4 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -49,7 +49,7 @@ This filter calls its configured `AuthenticationConverter` to create a `Saml2Aut This converter additionally resolves the <> 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 uses the <>. +By default, it uses the <>. image:{icondir}/number_3.png[] If authentication fails, then _Failure_. diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index e62657360c..27de151381 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -354,8 +354,8 @@ Java:: ---- @Bean Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { - OpenSaml4LogoutRequestResolver logoutRequestResolver = - new OpenSaml4LogoutRequestResolver(registrations); + OpenSaml5LogoutRequestResolver logoutRequestResolver = + new OpenSaml5LogoutRequestResolver(registrations); logoutRequestResolver.setParametersConsumer((parameters) -> { String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute"); String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; @@ -374,7 +374,7 @@ Kotlin:: ---- @Bean open fun logoutRequestResolver(registrations:RelyingPartyRegistrationRepository?): Saml2LogoutRequestResolver { - val logoutRequestResolver = OpenSaml4LogoutRequestResolver(registrations) + val logoutRequestResolver = OpenSaml5LogoutRequestResolver(registrations) logoutRequestResolver.setParametersConsumer { parameters: LogoutRequestParameters -> val name: String = (parameters.getAuthentication().getPrincipal() as Saml2AuthenticatedPrincipal).getFirstAttribute("CustomAttribute") val format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" @@ -439,8 +439,8 @@ Java:: ---- @Bean public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { - OpenSaml4LogoutResponseResolver logoutRequestResolver = - new OpenSaml4LogoutResponseResolver(registrations); + OpenSaml5LogoutResponseResolver logoutRequestResolver = + new OpenSaml5LogoutResponseResolver(registrations); logoutRequestResolver.setParametersConsumer((parameters) -> { if (checkOtherPrevailingConditions(parameters.getRequest())) { parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT); @@ -456,7 +456,7 @@ Kotlin:: ---- @Bean open fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?): Saml2LogoutResponseResolver { - val logoutRequestResolver = OpenSaml4LogoutResponseResolver(registrations) + val logoutRequestResolver = OpenSaml5LogoutResponseResolver(registrations) logoutRequestResolver.setParametersConsumer { LogoutResponseParameters parameters -> if (checkOtherPrevailingConditions(parameters.getRequest())) { parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT) @@ -605,7 +605,7 @@ Kotlin:: ---- @Component open class MyOpenSamlLogoutResponseValidator: Saml2LogoutResponseValidator { - private val delegate = OpenSaml4LogoutResponseValidator() + private val delegate = OpenSaml5LogoutResponseValidator() @Override fun logout(parameters: Saml2LogoutResponseValidatorParameters): Saml2LogoutResponseValidator { diff --git a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc index f495ad33a1..38d49d8657 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/metadata.adoc @@ -43,7 +43,7 @@ This allows three valuable features: * Implementations of `RelyingPartyRegistrationRepository` can more easily articulate a relationship between a relying party and its one or many corresponding asserting parties * Implementations can verify metadata signatures -For example, `OpenSaml4AssertingPartyMetadataRepository` uses OpenSAML's `MetadataResolver`, and API whose implementations regularly refresh the underlying metadata in an expiry-aware fashion. +For example, `OpenSaml5AssertingPartyMetadataRepository` uses OpenSAML's `MetadataResolver`, and API whose implementations regularly refresh the underlying metadata in an expiry-aware fashion. This means that you can now create a refreshable `RelyingPartyRegistrationRepository` in just a few lines of code: @@ -120,11 +120,11 @@ class RefreshableRelyingPartyRegistrationRepository : IterableRelyingPartyRegist ====== [TIP] -`OpenSaml4AssertingPartyMetadataRepository` also ships with a constructor so you can provide a custom `MetadataResolver`. Since the underlying `MetadataResolver` is doing the expiring and refreshing, if you use the constructor directly, you will only get these features by providing an implementation that does so. +`OpenSaml5AssertingPartyMetadataRepository` also ships with a constructor so you can provide a custom `MetadataResolver`. Since the underlying `MetadataResolver` is doing the expiring and refreshing, if you use the constructor directly, you will only get these features by providing an implementation that does so. === Verifying Metadata Signatures -You can also verify metadata signatures using `OpenSaml4AssertingPartyMetadataRepository` by providing the appropriate set of ``Saml2X509Credential``s as follows: +You can also verify metadata signatures using `OpenSaml5AssertingPartyMetadataRepository` by providing the appropriate set of ``Saml2X509Credential``s as follows: [tabs] ======