From 74fb626f748a20ffb07fd4c24cb571a6819d3998 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 19 Apr 2024 14:48:38 -0600 Subject: [PATCH] Revisit Saml2Logout Docs Issue gh-14944 --- .../pages/servlet/authorization/index.adoc | 1 + .../ROOT/pages/servlet/saml2/logout.adoc | 509 ++++++++++++++++-- 2 files changed, 459 insertions(+), 51 deletions(-) diff --git a/docs/modules/ROOT/pages/servlet/authorization/index.adoc b/docs/modules/ROOT/pages/servlet/authorization/index.adoc index 1a0579336b..f38085253f 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/index.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/index.adoc @@ -8,6 +8,7 @@ The advanced authorization capabilities within Spring Security represent one of Irrespective of how you choose to authenticate (whether using a Spring Security-provided mechanism and provider or integrating with a container or other non-Spring Security authentication authority), the authorization services can be used within your application in a consistent and simple way. You should consider attaching authorization rules to xref:servlet/authorization/authorize-http-requests.adoc[request URIs] and xref:servlet/authorization/method-security.adoc[methods] to begin. +In either case, you can listen and react to xref:servlet/authorization/events.adoc[authorization events] that each authorization check publishes. Below there is also wealth of detail about xref:servlet/authorization/architecture.adoc[how Spring Security authorization works] and how, having established a basic model, it can be fine-tuned. diff --git a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc index d737bf578a..73662a091d 100644 --- a/docs/modules/ROOT/pages/servlet/saml2/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/saml2/logout.adoc @@ -1,7 +1,7 @@ [[servlet-saml2login-logout]] = Performing Single Logout -Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout. +Among its xref:servlet/authentication/logout.adoc[other logout mechanisms], Spring Security ships with support for RP- and AP-initiated SAML 2.0 Single Logout. Briefly, there are two use cases Spring Security supports: @@ -22,61 +22,201 @@ To use Spring Security's SAML 2.0 Single Logout feature, you will need the follo * Second, the asserting party should be configured to sign and POST `saml2:LogoutRequest` s and `saml2:LogoutResponse` s your application's `/logout/saml2/slo` endpoint * Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s -You can begin from the initial minimal example and add the following configuration: +You can achieve this in Spring Boot in the following way: -[source,java] +[source,yaml] ---- -@Value("${private.key}") RSAPrivateKey key; -@Value("${public.certificate}") X509Certificate certificate; +spring: + security: + saml2: + relyingparty: + registration: + metadata: + signing.credentials: <3> + - private-key-location: classpath:credentials/rp-private.key + certificate-location: classpath:credentials/rp-certificate.crt + singlelogout.url: "{baseUrl}/logout/saml2/slo" <2> + assertingparty: + metadata-uri: https://ap.example.com/metadata <1> -@Bean -RelyingPartyRegistrationRepository registrations() { - Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate); - RelyingPartyRegistration registration = RelyingPartyRegistrations - .fromMetadataLocation("https://ap.example.org/metadata") - .registrationId("id") - .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") - .signingX509Credentials((signing) -> signing.add(credential)) <1> - .build(); - return new InMemoryRelyingPartyRegistrationRepository(registration); -} - -@Bean -SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { - http - .authorizeHttpRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .saml2Login(withDefaults()) - .saml2Logout(withDefaults()); <2> - - return http.build(); -} ---- -<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 +<1> - The metadata URI of the IDP, which will indicate to your application its support of SLO +<2> - The SLO endpoint in your application +<3> - The signing credentials to sign ````s and ````s + +[NOTE] +---- +An asserting party supports Single Logout if their metadata includes the `` element in their metadata. +---- + +And that's it! + +Spring Security's logout support offers a number of configuration points. +Consider the following use cases: + +* Understand how the above <<_startup_expectations, minimal configuration works>> +* Get a picture of <> +* Allow users to <> +* Customize <<_configuring_logout_endpoints, logout endpoints>> +* Storing `` somewhere <<_customizing_storage, other than the session>> + +=== Startup Expectations + +When these properties are used, in addition to login, SAML 2.0 Service Provider will automatically configure itself facilitate logout by way of ````s and ````s using either RP- or AP-initiated logout. + +It achieves this through a deterministic startup process: + +1. Query the Identity Server Metadata endpoint for the `` element +2. Scan the metadata and cache any public signature verification keys +3. Prepare the appropriate endpoints + +A consequence of this process is that the identity server must be up and receiving requests in order for Service Provider to successfully start up. + +[NOTE] +If the identity server is down when Service Provider queries it (given appropriate timeouts), then startup will fail. === Runtime Expectations -Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO. +Given the above configuration any logged-in user can send a `POST /logout` to your application to perform RP-initiated SLO. 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/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 +2. Produce a `` and POST it to the associated asserting party's SLO endpoint +3. Then, if the asserting party responds with a ``, the application with verify it and redirect to the configured success endpoint -Also, your application can participate in an AP-initiated logout when the asserting party sends a `` to `/logout/saml2/slo`: +Also, your application can participate in an AP-initiated logout when the asserting party sends a `` to `/logout/saml2/slo`. +When this happens, your application will do the following: -1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `` sent by the asserting party +1. Verify the `` 2. Logout the user and invalidate the session -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`] +3. Produce a `` and POST it back to the asserting party's SLO endpoint -NOTE: Adding `saml2Logout` adds the capability for logout to the service provider. +== Minimal Configuration Sans Boot + +Instead of Boot properties, you can also achieve the same outcome by publishing the beans directly like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Configuration +public class SecurityConfig { + @Value("${private.key}") RSAPrivateKey key; + @Value("${public.certificate}") X509Certificate certificate; + + @Bean + RelyingPartyRegistrationRepository registrations() { + Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate); + RelyingPartyRegistration registration = RelyingPartyRegistrations + .fromMetadataLocation("https://ap.example.org/metadata") <1> + .registrationId("metadata") + .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") <2> + .signingX509Credentials((signing) -> signing.add(credential)) <3> + .build(); + return new InMemoryRelyingPartyRegistrationRepository(registration); + } + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + .saml2Login(withDefaults()) + .saml2Logout(withDefaults()); <4> + + return http.build(); + } +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Configuration +class SecurityConfig(@Value("${private.key}") val key: RSAPrivateKey, + @Value("${public.certificate}") val certificate: X509Certificate) { + + @Bean + fun registrations(): RelyingPartyRegistrationRepository { + val credential = Saml2X509Credential.signing(key, certificate) + val registration = RelyingPartyRegistrations + .fromMetadataLocation("https://ap.example.org/metadata") <1> + .registrationId("metadata") + .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") <2> + .signingX509Credentials({ signing: List -> signing.add(credential) }) <3> + .build() + return InMemoryRelyingPartyRegistrationRepository(registration) + } + + @Bean + fun web(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + anyRequest = authenticated + } + saml2Login { + + } + saml2Logout { <4> + + } + } + + return http.build() + } +} +---- +====== +<1> - The metadata URI of the IDP, which will indicate to your application its support of SLO +<2> - The SLO endpoint in your application +<3> - The signing credentials to sign ````s and ````s, which you can also add to xref:servlet/saml2/login/overview.adoc#servlet-saml2login-rpr-duplicated[multiple relying parties] +<4> - Second, indicate that your application wants to use SAML SLO to logout the end user + +[NOTE] +Adding `saml2Logout` adds the capability for logout to your service provider as a whole. Because it is an optional capability, you need to enable it for each individual `RelyingPartyRegistration`. -You can do this by setting the `RelyingPartyRegistration.Builder#singleLogoutServiceLocation` property. +You do this by setting the `RelyingPartyRegistration.Builder#singleLogoutServiceLocation` property as seen above. + +[[architecture]] +== How Saml 2.0 Logout Works + +Next, let's see the architectural components that Spring Security uses to support http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf#page=37[SAML 2.0 Logout] in servlet-based applications, like the one we just saw. + +For RP-initiated logout: + +image:{icondir}/number_1.png[] Spring Security executes its xref:servlet/authentication/logout.adoc#logout-architecture[logout flow], calling its ``LogoutHandler``s to invalidate the session and perform other cleanup. +It then invokes the {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2RelyingPartyInitiatedLogoutSuccessHandler.html[`Saml2RelyingPartyInitiatedLogoutSuccessHandler`]. + +image:{icondir}/number_2.png[] The logout success handler uses an instance of +{security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestResolver.html[`Saml2LogoutRequestResolver`] to create, sign, and serialize a ``. +It uses the keys and configuration from the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] that is associated with the current `Saml2AuthenticatedPrincipal`. +Then, it redirect-POSTs the `` to the asserting party SLO endpoint + +The browser hands control over to the asserting party. +If the asserting party redirects back (which it may not), then the application proceeds to step image:{icondir}/number_3.png[]. + +image:{icondir}/number_3.png[] The {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseFilter.html[`Saml2LogoutResponseFilter`] deserializes, verifies, and processes the `` with its {security-api-url}org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutResponseValidator.html[`Saml2LogoutResponseValidator`]. + +image:{icondir}/number_4.png[] If valid, then it completes the local logout flow by redirecting to `/login?logout`, or whatever has been configured. +If invalid, then it responds with a 400. + +For AP-initiated logout: + +image:{icondir}/number_1.png[] The {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutRequestFilter.html[`Saml2LogoutRequestFilter`] deserializes, verifies, and processes the `` with its {security-api-url}org/springframework/security/saml2/provider/service/authentication/logout/Saml2LogoutRequestValidator.html[`Saml2LogoutRequestValidator`]. + +image:{icondir}/number_2.png[] If valid, then the filter calls the configured ``LogoutHandler``s, invalidating the session and performing other cleanup. + +image:{icondir}/number_3.png[] It uses a {security-api-url}org/springframework/security/saml2/provider/service/web/authentication/logout/Saml2LogoutResponseResolver.html[`Saml2LogoutResponseResolver`] to create, sign and serialize a ``. +It uses the keys and configuration from the xref:servlet/saml2/login/overview.adoc#servlet-saml2login-relyingpartyregistration[`RelyingPartyRegistration`] derived from the endpoint or from the contents of the ``. +Then, it redirect-POSTs the `` to the asserting party SLO endpoint. + +The browser hands control over to the asserting party. + +image:{icondir}/number_4.png[] If invalid, then it https://github.com/spring-projects/spring-security/pull/14676[responds with a 400]. == Configuring Logout Endpoints @@ -112,10 +252,87 @@ http .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")) ); ---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutRequest { + logoutUrl = "/SLOService.saml2" + } + logoutResponse { + logoutUrl = "/SLOService.saml2" + } + } +} +---- ====== You should also configure these endpoints in your `RelyingPartyRegistration`. +Also, you can customize the endpoint for triggering logout locally like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout")); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutUrl = "/saml2/logout" + } +} +---- +====== + +[[separating-local-saml2-logout]] +=== Separating Local Logout from SAML 2.0 Logout + +In some cases, you may want to expose one logout endpoint for local logout and another for RP-initiated SLO. +Like is the case with other logout mechanisms, you can register more than one, so long as they each have a different endpoint. + +So, for example, you can wire the DSL like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +http + .logout((logout) -> logout.logoutUrl("/logout")) + .saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout")); +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + logout { + logoutUrl = "/logout" + } + saml2Logout { + logoutUrl = "/saml2/logout" + } +} +---- +====== + +and now if a client sends a `POST /logout`, the session will be cleared, but there won't be a `` sent to the asserting party. +But, if the client sends a `POST /saml2/logout`, then the application will initiate SAML 2.0 SLO as normal. + == Customizing `` Resolution It's common to need to set other values in the `` than the defaults that Spring Security provides. @@ -129,7 +346,11 @@ By default, Spring Security will issue a `` and supply: To add other values, you can use delegation, like so: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- @Bean Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) { @@ -147,9 +368,33 @@ Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationReposit } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun logoutRequestResolver(registrations:RelyingPartyRegistrationRepository?): Saml2LogoutRequestResolver { + val logoutRequestResolver = OpenSaml4LogoutRequestResolver(registrations) + logoutRequestResolver.setParametersConsumer { parameters: LogoutRequestParameters -> + val name: String = (parameters.getAuthentication().getPrincipal() as Saml2AuthenticatedPrincipal).getFirstAttribute("CustomAttribute") + val format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + val logoutRequest: LogoutRequest = parameters.getLogoutRequest() + val nameId: NameID = logoutRequest.getNameID() + nameId.setValue(name) + nameId.setFormat(format) + } + return logoutRequestResolver +} +---- +====== + Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- http .saml2Logout((saml2) -> saml2 @@ -159,6 +404,20 @@ http ); ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutRequest { + logoutRequestResolver = this.logoutRequestResolver + } + } +} +---- +====== + == Customizing `` Resolution It's common to need to set other values in the `` than the defaults that Spring Security provides. @@ -172,7 +431,11 @@ By default, Spring Security will issue a `` and supply: To add other values, you can use delegation, like so: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- @Bean public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) { @@ -187,9 +450,30 @@ public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrati } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +open fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?): Saml2LogoutResponseResolver { + val logoutRequestResolver = OpenSaml4LogoutResponseResolver(registrations) + logoutRequestResolver.setParametersConsumer { LogoutResponseParameters parameters -> + if (checkOtherPrevailingConditions(parameters.getRequest())) { + parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT) + } + } + return logoutRequestResolver +} +---- +====== + Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- http .saml2Logout((saml2) -> saml2 @@ -199,12 +483,30 @@ http ); ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutRequest { + logoutRequestResolver = this.logoutRequestResolver + } + } +} +---- +====== + == Customizing `` Authentication To customize validation, you can implement your own `Saml2LogoutRequestValidator`. At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutRequestValidator` like so: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- @Component public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { @@ -221,24 +523,66 @@ public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValid } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +open class MyOpenSamlLogoutRequestValidator: Saml2LogoutRequestValidator { + private val delegate = OpenSamlLogoutRequestValidator() + + @Override + fun logout(parameters: Saml2LogoutRequestValidatorParameters): Saml2LogoutRequestValidator { + // verify signature, issuer, destination, and principal name + val result = delegate.authenticate(authentication) + + val logoutRequest: LogoutRequest = // ... parse using OpenSAML + // perform custom validation + } +} +---- +====== + Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- http .saml2Logout((saml2) -> saml2 .logoutRequest((request) -> request - .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator) + .logoutRequestValidator(myOpenSamlLogoutRequestValidator) ) ); ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutRequest { + logoutRequestValidator = myOpenSamlLogoutRequestValidator + } + } +} +---- +====== + == Customizing `` Authentication To customize validation, you can implement your own `Saml2LogoutResponseValidator`. At this point, the validation is minimal, so you may be able to first delegate to the default `Saml2LogoutResponseValidator` like so: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- @Component public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { @@ -255,9 +599,33 @@ public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseVal } ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Component +open class MyOpenSamlLogoutResponseValidator: Saml2LogoutResponseValidator { + private val delegate = OpenSamlLogoutResponseValidator() + + @Override + fun logout(parameters: Saml2LogoutResponseValidatorParameters): Saml2LogoutResponseValidator { + // verify signature, issuer, destination, and status + val result = delegate.authenticate(authentication) + + val logoutResponse: LogoutResponse = // ... parse using OpenSAML + // perform custom validation + } +} +---- +====== + Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- http .saml2Logout((saml2) -> saml2 @@ -267,13 +635,31 @@ http ); ---- +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutResponse { + logoutResponseValidator = myOpenSamlLogoutResponseValidator + } + } +} +---- +====== + == Customizing `` storage When your application sends a ``, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `` can be verified. If you want to store logout requests in some place other than the session, you can supply your custom implementation in the DSL, like so: -[source,java] +[tabs] +====== +Java:: ++ +[source,java,role="primary"] ---- http .saml2Logout((saml2) -> saml2 @@ -282,3 +668,24 @@ http ) ); ---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +http { + saml2Logout { + logoutRequest { + logoutRequestRepository = myCustomLogoutRequestRepository + } + } +} +---- +====== + +[[jc-logout-references]] +== Further Logout-Related References + +- xref:servlet/test/mockmvc/logout.adoc#test-logout[Testing Logout] +- xref:servlet/integrations/servlet-api.adoc#servletapi-logout[HttpServletRequest.logout()] +- xref:servlet/exploits/csrf.adoc#csrf-considerations-logout[Logging Out] in section CSRF Caveats