diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index c0ade22026..f08028dcdf 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -63,7 +63,10 @@ **** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy] **** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens] ** xref:servlet/saml2/index.adoc[SAML2] -*** xref:servlet/saml2/login.adoc[SAML2 Log In] +*** xref:servlet/saml2/login/index.adoc[SAML2 Log In] +**** xref:servlet/saml2/login/overview.adoc[SAML2 Log In Overview] +**** xref:servlet/saml2/login/authentication-requests.adoc[SAML2 Authentication Requests] +**** xref:servlet/saml2/login/authentication.adoc[SAML2 Authentication Responses] *** xref:servlet/saml2/logout.adoc[SAML2 Logout] *** xref:servlet/saml2/metadata.adoc[SAML2 Metadata] ** xref:servlet/exploits/index.adoc[Protection Against Exploits] diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc new file mode 100644 index 0000000000..ba394250a4 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication-requests.adoc @@ -0,0 +1,293 @@ +[[servlet-saml2login-sp-initiated-factory]] += Producing ````s + +As stated earlier, Spring Security's SAML 2.0 support produces a `` 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 ``. + +[[servlet-saml2login-store-authn-request]] +== Changing How the `` 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 ``] 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 ``]. + +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 authenticationRequestRepository() { + return new CustomSaml2AuthenticationRequestRepository(); +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository { + return CustomSaml2AuthenticationRequestRepository() +} +---- +==== + +[[servlet-saml2login-sp-initiated-factory-signing]] +== Changing How the `` Gets Sent + +By default, Spring Security signs each `` and send it as a GET to the asserting party. + +Many asserting parties don't require a signed ``. +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 `` before sending. + +[[servlet-saml2login-sp-initiated-factory-algorithm]] +By default, Spring Security will sign the `` 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 -> + 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 `` 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` to build an `AuthnRequest` yourself, like so: + +==== +.Java +[source,java,role="primary"] +---- +@Component +public class AuthnRequestConverter implements + Converter { + + 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 { + 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 +} +---- +==== + diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc new file mode 100644 index 0000000000..2d6efa7ab2 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/saml2/login/authentication.adoc @@ -0,0 +1,384 @@ +[[servlet-saml2login-authenticate-responses]] += Authenticating ````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 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 { + val params: MutableMap = 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 `` 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. + +[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]] +For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `` 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 `` or the `` since you are doing those yourself. + +[[servlet-saml2login-opensamlauthenticationprovider-decryption]] +== Customizing Decryption + +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]. +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. +For example, if you have a separate service that decrypts the assertions in a ``, 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 ``, 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("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. diff --git a/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc new file mode 100644 index 0000000000..30b8a715e6 --- /dev/null +++ b/docs/modules/ROOT/pages/servlet/saml2/login/index.adoc @@ -0,0 +1,18 @@ +[[servlet-saml2login]] += SAML 2.0 Login +:page-section-summary-toc: 1 + +The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc). + +NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in +https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles]. + +[[servlet-saml2login-spring-security-history]] +Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project]. +In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper. +This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support]. + +[NOTE] +==== +A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository]. +==== diff --git a/docs/modules/ROOT/pages/servlet/saml2/login.adoc b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc similarity index 54% rename from docs/modules/ROOT/pages/servlet/saml2/login.adoc rename to docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc index 83007d0b09..e7198c64f9 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/login.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/login/overview.adoc @@ -1,22 +1,6 @@ -[[servlet-saml2login]] -= SAML 2.0 Login -:figures: images/servlet/saml2 -:icondir: images/icons - -The SAML 2.0 Login feature provides an application with the capability to act as a SAML 2.0 Relying Party, having users https://wiki.shibboleth.net/confluence/display/CONCEPT/FlowsAndConfig[log in] to the application by using their existing account at a SAML 2.0 Asserting Party (Okta, ADFS, etc). - -NOTE: SAML 2.0 Login is implemented by using the *Web Browser SSO Profile*, as specified in -https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf#page=15[SAML 2 Profiles]. - -[[servlet-saml2login-spring-security-history]] -Since 2009, support for relying parties has existed as an https://github.com/spring-projects/spring-security-saml/tree/1e013b07a7772defd6a26fcfae187c9bf661ee8f#spring-saml[extension project]. -In 2019, the process began to port that into https://github.com/spring-projects/spring-security[Spring Security] proper. -This process is similar to the one started in 2017 for xref:servlet/oauth2/index.adoc[Spring Security's OAuth 2.0 support]. - -[NOTE] -==== -A working sample for {gh-samples-url}/servlet/spring-boot/java/saml2-login[SAML 2.0 Login] is available in the {gh-samples-url}[Spring Security Samples repository]. -==== += SAML 2.0 Login Overview +:figures: servlet/saml2 +:icondir: icons Let's take a look at how SAML 2.0 Relying Party Authentication works within Spring Security. First, we see that, like xref:servlet/oauth2/oauth2-login.adoc[OAuth 2.0 Login], Spring Security takes the user to a third-party for performing authentication. @@ -32,7 +16,7 @@ image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`. image:{icondir}/number_3.png[] Since the user lacks authorization, the xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__. -The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[`LoginUrlAuthenticationEntryPoint`] which redirects to <` 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 `` generating endpoint], `Saml2WebSsoAuthenticationRequestFilter`. Or, if you've <>, it will first redirect to a picker page. image:{icondir}/number_4.png[] Next, the `Saml2WebSsoAuthenticationRequestFilter` creates, signs, serializes, and encodes a `` using its configured <>. @@ -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 `` to the application, it <>. +image:{icondir}/number_1.png[] When the browser submits a `` 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 <> 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: * <> -* <> +* xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticatedprincipal[How to Use the `Saml2AuthenticatedPrincipal`] * <> [[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 <>. +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 <>. +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 `` and any `` conditions. @@ -761,7 +745,7 @@ class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationR ---- ==== -Then, you can provide this resolver to the appropriate filters that <``s>>, <``s>>, and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `` 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 ````s], xref:servlet/saml2/login/authentication.adoc#servlet-saml2login-authenticate-responses[authenticate ````s], and xref:servlet/saml2/metadata.adoc#servlet-saml2login-metadata[produce `` 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 ````s - -As stated earlier, Spring Security's SAML 2.0 support produces a `` 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 ``. - -[[servlet-saml2login-store-authn-request]] -=== Changing How the `` Gets Stored - -`Saml2WebSsoAuthenticationRequestFilter` uses an `Saml2AuthenticationRequestRepository` to persist an `AbstractSaml2AuthenticationRequest` instance before <`>> to the asserting party. - -Additionally, `Saml2WebSsoAuthenticationFilter` and `Saml2AuthenticationTokenConverter` use an `Saml2AuthenticationRequestRepository` to load any `AbstractSaml2AuthenticationRequest` as part of <`>>. - -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 authenticationRequestRepository() { - return new CustomSaml2AuthenticationRequestRepository(); -} ----- - -.Kotlin -[source,kotlin,role="secondary"] ----- -@Bean -open fun authenticationRequestRepository(): Saml2AuthenticationRequestRepository { - return CustomSaml2AuthenticationRequestRepository() -} ----- -==== - -[[servlet-saml2login-sp-initiated-factory-signing]] -=== Changing How the `` Gets Sent - -By default, Spring Security signs each `` and send it as a GET to the asserting party. - -Many asserting parties don't require a signed ``. -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 `` before sending. - -[[servlet-saml2login-sp-initiated-factory-algorithm]] -By default, Spring Security will sign the `` 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 <>. - -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 -> - 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 `` 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 <>. -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` to build an `AuthnRequest` yourself, like so: - -==== -.Java -[source,java,role="primary"] ----- -@Component -public class AuthnRequestConverter implements - Converter { - - 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 { - 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 ````s - -To verify SAML 2.0 Responses, Spring Security uses <> 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 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 { - val params: MutableMap = 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 `` 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. - -[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]] -For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `` 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 `` or the `` since you are doing those yourself. - -[[servlet-saml2login-opensamlauthenticationprovider-decryption]] -=== Customizing Decryption - -Spring Security decrypts ``, ``, and `` elements automatically by using the decryption <> registered in the <>. - -`OpenSaml4AuthenticationProvider` exposes <>. -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. -For example, if you have a separate service that decrypts the assertions in a ``, 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 ``, 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("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. diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index 1de18fa31f..0d1a886753 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -52,7 +52,7 @@ SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository re return http.build(); } ---- -<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login.adoc#servlet-saml2login-rpr-duplicated[multiple instances] +<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-duplicated[multiple instances] <2> - Second, indicate that your application wants to use SAML SLO to logout the end user === Runtime Expectations @@ -61,8 +61,8 @@ Given the above configuration any logged in user can send a `POST /logout` to yo Your application will then do the following: 1. Logout the user and invalidate the session -2. Use a `Saml2LogoutRequestResolver` to create, sign, and serialize a `` 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 `` 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 `` 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 `` sent by the asserting party 2. Logout the user and invalidate the session -3. Create, sign, and serialize a `` 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 `` 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