mirror of
				https://github.com/spring-projects/spring-security.git
				synced 2025-10-30 22:28:46 +00:00 
			
		
		
		
	Separate SAML 2.0 Login Docs
Issue gh-10367
This commit is contained in:
		
							parent
							
								
									6d2d3b9a69
								
							
						
					
					
						commit
						11aa02c6fb
					
				| @ -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] | ||||
|  | ||||
| @ -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
									
								
								docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										384
									
								
								docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
								
								docs/modules/ROOT/pages/servlet/saml2/login/index.adoc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/modules/ROOT/pages/servlet/saml2/login/index.adoc
									
									
									
									
									
										Normal file
									
								
							| @ -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]. | ||||
| ==== | ||||
| @ -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. | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user