From 6f52baba29fa31c79bbe1b058f1cffe44fb5fab1 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 25 Mar 2021 10:44:26 -0600 Subject: [PATCH] Add Saml2LogoutConfigurer Closes gh-9497 --- .../annotation/web/builders/HttpSecurity.java | 137 ++++ .../saml2/Saml2LoginConfigurer.java | 4 + .../saml2/Saml2LogoutConfigurer.java | 637 ++++++++++++++++++ .../saml2/Saml2LogoutConfigurerTests.java | 400 +++++++++++ .../_includes/servlet/saml2/saml2-login.adoc | 255 +++---- 5 files changed, 1314 insertions(+), 119 deletions(-) create mode 100644 config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java create mode 100644 config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 0b11a22e90..3e0a5d6031 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -64,6 +64,7 @@ import org.springframework.security.config.annotation.web.configurers.oauth2.cli import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer; import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer; +import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -2121,6 +2122,142 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder + *
+ * + * Implements the Single Logout Profile, using POST and REDIRECT bindings, as + * documented in the + * SAML V2.0 + * Core,Profiles and Bindings specifications.
+ *
+ * + * As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting + * Party to sent a logout request to. The representation of the relying party and the + * asserting party is contained within {@link RelyingPartyRegistration}.
+ *
+ * + * {@link RelyingPartyRegistration}(s) are composed within a + * {@link RelyingPartyRegistrationRepository}, which is required and must be + * registered with the {@link ApplicationContext} or configured via + * saml2Logout().relyingPartyRegistrationRepository(..).
+ *
+ * + * The default configuration provides an auto-generated logout endpoint at + * "/saml2/logout" and redirects to /login?logout + * when logout completes.
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using SimpleSamlPhp + * as the asserting party. + * + *
+	 *	@EnableWebSecurity
+	 *	@Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		@Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeRequests((authorize) -> authorize
+	 *					.anyRequest().authenticated()
+	 *				)
+	 *				.saml2Login(withDefaults())
+	 *				.saml2Logout(withDefaults());
+	 *			return http.build();
+	 *		}
+	 *
+	 *		@Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * 
+ * + *

+ * @return the {@link Saml2LoginConfigurer} for further customizations + * @throws Exception + * @since 5.5 + */ + public HttpSecurity saml2Logout(Customizer> saml2LogoutCustomizer) + throws Exception { + saml2LogoutCustomizer.customize(getOrApply(new Saml2LogoutConfigurer<>(getContext()))); + return HttpSecurity.this; + } + + /** + * Configures logout support for an SAML 2.0 Relying Party.
+ *
+ * + * Implements the Single Logout Profile, using POST and REDIRECT bindings, as + * documented in the + * SAML V2.0 + * Core,Profiles and Bindings specifications.
+ *
+ * + * As a prerequisite to using this feature, is that you have a SAML v2.0 Asserting + * Party to sent a logout request to. The representation of the relying party and the + * asserting party is contained within {@link RelyingPartyRegistration}.
+ *
+ * + * {@link RelyingPartyRegistration}(s) are composed within a + * {@link RelyingPartyRegistrationRepository}, which is required and must be + * registered with the {@link ApplicationContext} or configured via + * saml2Logout().relyingPartyRegistrationRepository(..).
+ *
+ * + * The default configuration provides an auto-generated logout endpoint at + * "/saml2/logout" and redirects to /login?logout + * when logout completes.
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using SimpleSamlPhp + * as the asserting party. + * + *
+	 *	@EnableWebSecurity
+	 *	@Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		@Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeRequests((authorize) -> authorize
+	 *					.anyRequest().authenticated()
+	 *				)
+	 *				.saml2Login(withDefaults())
+	 *				.saml2Logout(withDefaults());
+	 *			return http.build();
+	 *		}
+	 *
+	 *		@Bean
+	 *		public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
+	 *			RelyingPartyRegistration registration = RelyingPartyRegistrations
+	 *					.withMetadataLocation("https://ap.example.org/metadata")
+	 *					.registrationId("simple")
+	 *					.build();
+	 *			return new InMemoryRelyingPartyRegistrationRepository(registration);
+	 *		}
+	 *	}
+	 * 
+ * + *

+ * @return the {@link Saml2LoginConfigurer} for further customizations + * @throws Exception + * @since 5.5 + */ + public Saml2LogoutConfigurer saml2Logout() throws Exception { + return getOrApply(new Saml2LogoutConfigurer<>(getContext())); + } + /** * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 * Provider.
diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 196878c578..e49a291a47 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -221,6 +221,10 @@ public final class Saml2LoginConfigurer> // Setup auto-redirect to provider login page // when only 1 IDP is configured this.updateAuthenticationDefaults(); + Saml2LogoutConfigurer logoutConfigurer = http.getConfigurer(Saml2LogoutConfigurer.class); + if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) { + logoutConfigurer.logoutSuccessUrl("/login?logout"); + } this.updateAccessDefaults(http); String loginUrl = providerUrlMap.entrySet().iterator().next().getKey(); final LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(loginUrl); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java new file mode 100644 index 0000000000..a89fd350b6 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -0,0 +1,637 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.saml2; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.opensaml.core.Version; + +import org.springframework.context.ApplicationContext; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestSuccessHandler; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseSuccessHandler; +import org.springframework.security.web.authentication.logout.CompositeLogoutHandler; +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.csrf.CsrfLogoutHandler; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; + +/** + * Adds SAML 2.0 logout support. + * + *

Security Filters

+ * + * The following Filters are populated + * + *
    + *
  • {@link LogoutFilter}
  • + *
  • {@link Saml2LogoutRequestFilter}
  • + *
  • {@link Saml2LogoutResponseFilter}
  • + *
+ * + *

+ * The following configuration options are available: + * + *

    + *
  • {@link #logoutUrl} - The URL to initiate SAML 2.0 Logout
  • + *
  • {@link #logoutRequestMatcher} - The {@link RequestMatcher} to initiate SAML 2.0 + * Logout
  • + *
  • {@link #logoutSuccessHandler} - The {@link LogoutSuccessHandler} to execute once + * SAML 2.0 Logout is complete
  • + *
  • {@link LogoutRequestConfigurer#logoutRequestMatcher} - The {@link RequestMatcher} + * to receive SAML 2.0 Logout Requests
  • + *
  • {@link LogoutRequestConfigurer#logoutHandler} - The {@link LogoutHandler} for + * processing SAML 2.0 Logout Requests
  • + *
  • {@link LogoutRequestConfigurer#logoutRequestResolver} - The + * {@link Saml2LogoutRequestResolver} for creating SAML 2.0 Logout Requests
  • + *
  • {@link LogoutRequestConfigurer#logoutRequestRepository} - The + * {@link Saml2LogoutRequestRepository} for storing SAML 2.0 Logout Requests
  • + *
  • {@link LogoutResponseConfigurer#logoutRequestMatcher} - The {@link RequestMatcher} + * to receive SAML 2.0 Logout Responses
  • + *
  • {@link LogoutResponseConfigurer#logoutHandler} - The {@link LogoutHandler} for + * processing SAML 2.0 Logout Responses
  • + *
  • {@link LogoutResponseConfigurer#logoutResponseResolver} - The + * {@link Saml2LogoutResponseResolver} for creating SAML 2.0 Logout Responses
  • + *
+ * + *

Shared Objects Created

+ * + * No shared Objects are created + * + *

Shared Objects Used

+ * + * Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}. + * + * @author Josh Cummings + * @since 5.5 + * @see Saml2LogoutConfigurer + */ +public final class Saml2LogoutConfigurer> + extends AbstractHttpConfigurer, H> { + + private ApplicationContext context; + + private List logoutHandlers = new ArrayList<>(); + + private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler(); + + private String logoutSuccessUrl = "/login?logout"; + + private LogoutSuccessHandler logoutSuccessHandler; + + private String logoutUrl = "/logout"; + + private RequestMatcher logoutRequestMatcher; + + private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + private LogoutRequestConfigurer logoutRequestConfigurer; + + private LogoutResponseConfigurer logoutResponseConfigurer; + + /** + * Creates a new instance + * @see HttpSecurity#logout() + */ + public Saml2LogoutConfigurer(ApplicationContext context) { + this.context = context; + this.logoutRequestConfigurer = new LogoutRequestConfigurer(); + this.logoutResponseConfigurer = new LogoutResponseConfigurer(this.logoutRequestConfigurer); + } + + /** + * Adds a {@link LogoutHandler}. {@link SecurityContextLogoutHandler} and + * {@link LogoutSuccessEventPublishingLogoutHandler} are added as last + * {@link LogoutHandler} instances by default. + * @param logoutHandler the {@link LogoutHandler} to add + * @return the {@link Saml2LogoutConfigurer} for further customization + */ + public Saml2LogoutConfigurer addLogoutHandler(LogoutHandler logoutHandler) { + Assert.notNull(logoutHandler, "logoutHandler cannot be null"); + this.logoutHandlers.add(logoutHandler); + return this; + } + + /** + * Specifies if {@link SecurityContextLogoutHandler} should clear the + * {@link Authentication} at the time of logout. + * @param clearAuthentication true {@link SecurityContextLogoutHandler} should clear + * the {@link Authentication} (default), or false otherwise. + * @return the {@link Saml2LogoutConfigurer} for further customization + */ + public Saml2LogoutConfigurer clearAuthentication(boolean clearAuthentication) { + this.contextLogoutHandler.setClearAuthentication(clearAuthentication); + return this; + } + + /** + * Configures {@link SecurityContextLogoutHandler} to invalidate the + * {@link HttpSession} at the time of logout. + * @param invalidateHttpSession true if the {@link HttpSession} should be invalidated + * (default), or false otherwise. + * @return the {@link Saml2LogoutConfigurer} for further customization + */ + public Saml2LogoutConfigurer invalidateHttpSession(boolean invalidateHttpSession) { + this.contextLogoutHandler.setInvalidateHttpSession(invalidateHttpSession); + return this; + } + + /** + * The URL that triggers log out to occur (default is "/logout"). If CSRF protection + * is enabled (default), then the request must also be a POST. This means that by + * default POST "/logout" is required to trigger a log out. If CSRF protection is + * disabled, then any HTTP method is allowed. + * + *

+ * It is considered best practice to use an HTTP POST on any action that changes state + * (i.e. log out) to protect against + * CSRF + * attacks. If you really want to use an HTTP GET, you can use + * logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET")); + *

+ * @param logoutUrl the URL that will invoke logout. + * @return the {@link Saml2LogoutConfigurer} for further customization + * @see #logoutRequestMatcher(RequestMatcher) + * @see HttpSecurity#csrf() + */ + public Saml2LogoutConfigurer logoutUrl(String logoutUrl) { + this.logoutRequestMatcher = null; + this.logoutUrl = logoutUrl; + return this; + } + + /** + * The RequestMatcher that triggers log out to occur. In most circumstances users will + * use {@link #logoutUrl(String)} which helps enforce good practices. + * @param logoutRequestMatcher the RequestMatcher used to determine if logout should + * occur. + * @return the {@link Saml2LogoutConfigurer} for further customization + * @see #logoutUrl(String) + */ + public Saml2LogoutConfigurer logoutRequestMatcher(RequestMatcher logoutRequestMatcher) { + this.logoutUrl = null; + this.logoutRequestMatcher = logoutRequestMatcher; + return this; + } + + /** + * The URL to redirect to after logout has occurred. The default is "/login?logout". + * This is a shortcut for invoking {@link #logoutSuccessHandler(LogoutSuccessHandler)} + * with a {@link SimpleUrlLogoutSuccessHandler}. + * @param logoutSuccessUrl the URL to redirect to after logout occurred + * @return the {@link Saml2LogoutConfigurer} for further customization + */ + public Saml2LogoutConfigurer logoutSuccessUrl(String logoutSuccessUrl) { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); + this.logoutSuccessHandler = logoutSuccessHandler; + return this; + } + + /** + * Sets the {@link LogoutSuccessHandler} to use. If this is specified, + * {@link #logoutSuccessUrl(String)} is ignored. + * @param logoutSuccessHandler the {@link LogoutSuccessHandler} to use after a user + * has been logged out. + * @return the {@link Saml2LogoutConfigurer} for further customizations + */ + public Saml2LogoutConfigurer logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) { + this.logoutSuccessHandler = logoutSuccessHandler; + return this; + } + + /** + * Allows specifying the names of cookies to be removed on logout success. This is a + * shortcut to easily invoke {@link #addLogoutHandler(LogoutHandler)} with a + * {@link CookieClearingLogoutHandler}. + * @param cookieNamesToClear the names of cookies to be removed on logout success. + * @return the {@link Saml2LogoutConfigurer} for further customization + */ + public Saml2LogoutConfigurer deleteCookies(String... cookieNamesToClear) { + return addLogoutHandler(new CookieClearingLogoutHandler(cookieNamesToClear)); + } + + /** + * Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party + * representing a service provider, SP and this host, and identity provider, IDP pair + * that communicate with each other. + * @param repo the repository of relying parties + * @return the {@link Saml2LoginConfigurer} for further configuration + */ + public Saml2LogoutConfigurer relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) { + this.relyingPartyRegistrationRepository = repo; + return this; + } + + /** + * Get configurer for SAML 2.0 Logout Request components + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequest() { + return this.logoutRequestConfigurer; + } + + /** + * Configures SAML 2.0 Logout Request components + * @param logoutRequestConfigurerCustomizer the {@link Customizer} to provide more + * options for the {@link LogoutRequestConfigurer} + * @return the {@link Saml2LogoutConfigurer} for further customizations + */ + public Saml2LogoutConfigurer logoutRequest( + Customizer logoutRequestConfigurerCustomizer) { + logoutRequestConfigurerCustomizer.customize(this.logoutRequestConfigurer); + return this; + } + + /** + * Get configurer for SAML 2.0 Logout Response components + * @return the {@link LogoutResponseConfigurer} for further customizations + */ + public LogoutResponseConfigurer logoutResponse() { + return this.logoutResponseConfigurer; + } + + /** + * Configures SAML 2.0 Logout Request components + * @param logoutResponseConfigurerCustomizer the {@link Customizer} to provide more + * options for the {@link LogoutResponseConfigurer} + * @return the {@link Saml2LogoutConfigurer} for further customizations + */ + public Saml2LogoutConfigurer logoutResponse( + Customizer logoutResponseConfigurerCustomizer) { + logoutResponseConfigurerCustomizer.customize(this.logoutResponseConfigurer); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void configure(H http) throws Exception { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = getRelyingPartyRegistrationResolver(http); + CsrfTokenRepository csrfTokenRepository = http.getSharedObject(CsrfTokenRepository.class); + if (csrfTokenRepository != null) { + this.logoutHandlers.add(new CsrfLogoutHandler(csrfTokenRepository)); + } + this.logoutHandlers.add(this.contextLogoutHandler); + this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler())); + LogoutFilter logoutFilter = createLogoutFilter(http, this.logoutHandlers, relyingPartyRegistrationResolver); + http.addFilterBefore(logoutFilter, LogoutFilter.class); + Saml2LogoutRequestFilter logoutRequestFilter = createLogoutRequestFilter(this.logoutHandlers, + relyingPartyRegistrationResolver); + http.addFilterBefore(logoutRequestFilter, LogoutFilter.class); + Saml2LogoutResponseFilter logoutResponseFilter = createLogoutResponseFilter(relyingPartyRegistrationResolver); + logoutResponseFilter.setLogoutSuccessHandler(getLogoutSuccessHandler()); + http.addFilterBefore(logoutResponseFilter, LogoutFilter.class); + } + + /** + * Returns true if the logout success has been customized via + * {@link #logoutSuccessUrl(String)} or + * {@link #logoutSuccessHandler(LogoutSuccessHandler)}. + * @return true if logout success handling has been customized, else false + */ + boolean isCustomLogoutSuccess() { + return this.logoutSuccessHandler != null; + } + + private RelyingPartyRegistrationResolver getRelyingPartyRegistrationResolver(H http) { + RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(); + return new DefaultRelyingPartyRegistrationResolver(registrations); + } + + private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository() { + if (this.relyingPartyRegistrationRepository == null) { + this.relyingPartyRegistrationRepository = getBeanOrNull(RelyingPartyRegistrationRepository.class); + } + return this.relyingPartyRegistrationRepository; + } + + private LogoutFilter createLogoutFilter(H http, List logoutHandlers, + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + LogoutHandler[] handlers = logoutHandlers.toArray(new LogoutHandler[0]); + LogoutSuccessHandler logoutRequestSuccessHandler = this.logoutRequestConfigurer + .logoutRequestSuccessHandler(relyingPartyRegistrationResolver); + LogoutSuccessHandler finalSuccessHandler = getLogoutSuccessHandler(); + LogoutSuccessHandler logoutSuccessHandler = (request, response, authentication) -> { + if (authentication == null) { + finalSuccessHandler.onLogoutSuccess(request, response, authentication); + } + else { + logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication); + } + }; + LogoutFilter result = new LogoutFilter(logoutSuccessHandler, handlers) { + @Override + protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (!(authentication instanceof Saml2Authentication)) { + return false; + } + return super.requiresLogout(request, response); + } + }; + result.setLogoutRequestMatcher(getLogoutRequestMatcher(http)); + return postProcess(result); + } + + private Saml2LogoutRequestFilter createLogoutRequestFilter(List logoutHandlers, + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + LogoutHandler logoutRequestHandler = this.logoutRequestConfigurer + .logoutRequestHandler(relyingPartyRegistrationResolver); + List handlers = new ArrayList<>(); + handlers.add(logoutRequestHandler); + handlers.addAll(logoutHandlers); + Saml2LogoutRequestFilter logoutRequestFilter = new Saml2LogoutRequestFilter( + this.logoutResponseConfigurer.logoutResponseSuccessHandler(relyingPartyRegistrationResolver), + new CompositeLogoutHandler(handlers)); + logoutRequestFilter.setLogoutRequestMatcher(this.logoutRequestConfigurer.requestMatcher); + CsrfConfigurer csrf = getBuilder().getConfigurer(CsrfConfigurer.class); + if (csrf != null) { + csrf.ignoringRequestMatchers(this.logoutRequestConfigurer.requestMatcher); + } + return logoutRequestFilter; + } + + private Saml2LogoutResponseFilter createLogoutResponseFilter( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter( + this.logoutResponseConfigurer.logoutResponseHandler(relyingPartyRegistrationResolver)); + logoutResponseFilter.setLogoutRequestMatcher(this.logoutResponseConfigurer.requestMatcher); + CsrfConfigurer csrf = getBuilder().getConfigurer(CsrfConfigurer.class); + if (csrf != null) { + csrf.ignoringRequestMatchers(this.logoutResponseConfigurer.requestMatcher); + } + logoutResponseFilter.setLogoutSuccessHandler(getLogoutSuccessHandler()); + return logoutResponseFilter; + } + + private RequestMatcher getLogoutRequestMatcher(H http) { + if (this.logoutRequestMatcher != null) { + return this.logoutRequestMatcher; + } + this.logoutRequestMatcher = createLogoutRequestMatcher(http); + return this.logoutRequestMatcher; + } + + @SuppressWarnings("unchecked") + private RequestMatcher createLogoutRequestMatcher(H http) { + RequestMatcher post = createLogoutRequestMatcher("POST"); + if (http.getConfigurer(CsrfConfigurer.class) != null) { + return post; + } + RequestMatcher get = createLogoutRequestMatcher("GET"); + return new OrRequestMatcher(get, post); + } + + private RequestMatcher createLogoutRequestMatcher(String httpMethod) { + return new AntPathRequestMatcher(this.logoutUrl, httpMethod); + } + + private LogoutSuccessHandler getLogoutSuccessHandler() { + if (this.logoutSuccessHandler != null) { + return this.logoutSuccessHandler; + } + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl(this.logoutSuccessUrl); + this.logoutSuccessHandler = logoutSuccessHandler; + return logoutSuccessHandler; + } + + private C getBeanOrNull(Class clazz) { + if (this.context == null) { + return null; + } + if (this.context.getBeanNamesForType(clazz).length == 0) { + return null; + } + return this.context.getBean(clazz); + } + + /** + * A configurer for SAML 2.0 LogoutRequest components + */ + public final class LogoutRequestConfigurer { + + private RequestMatcher requestMatcher = new AntPathRequestMatcher("/logout/saml2/slo"); + + private LogoutHandler logoutHandler; + + private LogoutSuccessHandler logoutSuccessHandler; + + private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository(); + + LogoutRequestConfigurer() { + } + + /** + * Use this {@link RequestMatcher} for recognizing a logout request from the + * asserting party + * + *

+ * Defaults to {@code /logout/saml2} + * @param requestMatcher the {@link RequestMatcher} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + return this; + } + + /** + * Use this {@link LogoutHandler} for processing a logout request from the + * asserting party + * @param logoutHandler the {@link LogoutHandler} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequestHandler(LogoutHandler logoutHandler) { + this.logoutHandler = logoutHandler; + return this; + } + + /** + * Use this {@link Saml2LogoutRequestResolver} for producing a logout request to + * send to the asserting party + * @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequestResolver(Saml2LogoutRequestResolver logoutRequestResolver) { + this.logoutSuccessHandler = new Saml2LogoutRequestSuccessHandler(logoutRequestResolver); + return this; + } + + /** + * Use this {@link Saml2LogoutRequestRepository} for storing logout requests + * @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) { + this.logoutRequestRepository = logoutRequestRepository; + return this; + } + + public Saml2LogoutConfigurer and() { + return Saml2LogoutConfigurer.this; + } + + private LogoutHandler logoutRequestHandler(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutHandler == null) { + return new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver); + } + return this.logoutHandler; + } + + private LogoutSuccessHandler logoutRequestSuccessHandler( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutSuccessHandler == null) { + Saml2LogoutRequestSuccessHandler logoutSuccessHandler = new Saml2LogoutRequestSuccessHandler( + logoutRequestResolver(relyingPartyRegistrationResolver)); + logoutSuccessHandler.setLogoutRequestRepository(this.logoutRequestRepository); + return logoutSuccessHandler; + } + return this.logoutSuccessHandler; + } + + private Saml2LogoutRequestResolver logoutRequestResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (Version.getVersion().startsWith("4")) { + return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver); + } + return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver); + } + + } + + public final class LogoutResponseConfigurer { + + private final LogoutRequestConfigurer logoutRequest; + + private RequestMatcher requestMatcher = new AntPathRequestMatcher("/logout/saml2/slo"); + + private LogoutHandler logoutHandler; + + private LogoutSuccessHandler logoutSuccessHandler; + + LogoutResponseConfigurer(LogoutRequestConfigurer logoutRequest) { + this.logoutRequest = logoutRequest; + } + + /** + * Use this {@link RequestMatcher} for recognizing a logout response from the + * asserting party + * + *

+ * Defaults to {@code /logout/saml2} + * @param requestMatcher the {@link RequestMatcher} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutResponseConfigurer logoutRequestMatcher(RequestMatcher requestMatcher) { + this.requestMatcher = requestMatcher; + return this; + } + + /** + * Use this {@link LogoutHandler} for processing a logout response from the + * asserting party + * @param logoutHandler the {@link LogoutHandler} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutResponseConfigurer logoutResponseHandler(LogoutHandler logoutHandler) { + this.logoutHandler = logoutHandler; + return this; + } + + /** + * Use this {@link Saml2LogoutRequestResolver} for producing a logout response to + * send to the asserting party + * @param logoutResponseResolver the {@link Saml2LogoutResponseResolver} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutResponseConfigurer logoutResponseResolver(Saml2LogoutResponseResolver logoutResponseResolver) { + this.logoutSuccessHandler = new Saml2LogoutResponseSuccessHandler(logoutResponseResolver); + return this; + } + + public Saml2LogoutConfigurer and() { + return Saml2LogoutConfigurer.this; + } + + private LogoutHandler logoutResponseHandler(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutHandler == null) { + OpenSamlLogoutResponseHandler logoutHandler = new OpenSamlLogoutResponseHandler( + relyingPartyRegistrationResolver); + logoutHandler.setLogoutRequestRepository(this.logoutRequest.logoutRequestRepository); + return logoutHandler; + } + return this.logoutHandler; + } + + private LogoutSuccessHandler logoutResponseSuccessHandler( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutSuccessHandler == null) { + return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver(relyingPartyRegistrationResolver)); + } + return this.logoutSuccessHandler; + } + + private Saml2LogoutResponseResolver logoutResponseResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (Version.getVersion().startsWith("4")) { + return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver); + } + return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java new file mode 100644 index 0000000000..b3bb2d1baf --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -0,0 +1,400 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.config.annotation.web.configurers.saml2; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.opensaml.saml.saml2.core.LogoutRequest; +import org.opensaml.xmlsec.signature.support.SignatureConstants; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.saml2.Saml2Exception; +import org.springframework.security.saml2.core.Saml2X509Credential; +import org.springframework.security.saml2.core.TestSaml2X509Credentials; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; +import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; +import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver.Saml2LogoutResponseBuilder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.RETURNS_SELF; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.reset; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.verifyNoInteractions; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for different Java configuration for {@link Saml2LogoutConfigurer} + */ +public class Saml2LogoutConfigurerTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Autowired + private RelyingPartyRegistrationRepository repository; + + private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository(); + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired(required = false) + MockMvc mvc; + + private Saml2Authentication user = new Saml2Authentication( + new DefaultSaml2AuthenticatedPrincipal("user", Collections.emptyMap()), "response", + AuthorityUtils.createAuthorityList("ROLE_USER"), "registration-id"); + + String apLogoutRequest = "nZFBa4MwGIb/iuQeE2NTXFDLQAaC26Hrdtgt1dQFNMnyxdH9+zlboeyww275SN7nzcOX787jEH0qD9qaAiUxRZEyre206Qv0cnjAGdqVOchxYE40trdT2KuPSUGI5qQBcbkq0OSNsBI0CCNHBSK04vn+sREspsJ5G2xrBxRVc1AbGZa29xAcCEK8i9VZjm5QsfU9GZYWsoCJv5ShqK4K1Ow5p5LyU4aP6XaLN3cpw9mGctydjrxNaZt1XM5vASZVGwjShAIxyhJMU8z4gSWCM8GSmDH+hqLX1Xv+JLpaiiXsb+3+lpMAyv8IoVI6rEzQ4QvrLie3uBX+NMfr6l/waT6t0AumvI6/FlN+Aw=="; + + String apLogoutRequestSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + + String apLogoutRequestRelayState = "33591874-b123-4f2c-ab0d-2d0d84aa8b56"; + + String apLogoutRequestSignature = "oKqdzrmn2YAqXcwkow2lzRXr5PNHm0s/gWsRnaZYhC+Oq5ekK5uIKQYvtmNR94HJjDe1VRs+vVQCYivgdoTzBV2ZlffTXZmYsCsY9q4jbCWR6R5CbhU73/MkKQsPcyVvMhNYxnDYapIlxDsfoZNTboDEz3GM+HRoGRfl9emCXY0lPRYwqC4kpu7oMDBkafR0A09jPIxFuNpqlLPwUxL9m+DGkvDK3mFDN1xJcgZaK73HcuJe7Qh4huOrKNFetwc5EvqfiwgiWF6sfq9A+rZBfCIYo10NNLY7fNQAR2IqwcKtawHgTGWbeshRyFrwVYMR64EnClfxUHsHKf5kiZ2dlw=="; + + String apLogoutResponse = "fZHRa4MwEMb/Fcl7jEadGqplrAwK3Uvb9WFvZ4ydoInk4uj++1nXbmWMvhwcd9/3Jb9bLE99530oi63RBQn9gHhKS1O3+liQ1/0zzciyXCD0HR/ExhzN6LYKB6NReZNUo/ieFWS0WhjAFoWGXqFwUuweXzaC+4EYrHFGmo54K4Wu1eDmuHfnBhSM2cFXJ+iHTvnGHlk3x7DZmNlLGvHWq4Jstk0GUSjjiIZJI2lcpQnNeRLTAOo4fwCeQg3Trr6+cm/OqmnWVHECVGWQ0jgCSatsKvXUxhFvZF7xSYU4qrVGB9oVhAc8pEFEebLnkeBc8NyPePpGvMOV1/Q3cqEjZrG9hXKfCSAqe+ZAShio0q51n7StF+zW7gf9zoEb8U/7ZGrlHaAb1f0onLfFbpRSIRJWXkJ+bdm/Fy6/AA=="; + + String apLogoutResponseSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + + String apLogoutResponseRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315"; + + String apLogoutResponseSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q=="; + + String rpLogoutRequest = "nZFBa4MwGIb/iuQeY6NlGtQykIHgdui6HXaLmrqAJlm+OLp/v0wrlB122CXkI3mfNw/JD5dpDD6FBalVgXZhhAKhOt1LNRTo5fSAU3Qoc+DTSA1r9KBndxQfswAX+KQCth4VaLaKaQ4SmOKTAOY69nz/2DAaRsxY7XSnRxRUPigVd0vbu3MGGCHchOLCJzOKUNuBjEsLWcDErmUoqKsCNcc+yc5tsudYpPwOJzHvcJv6pfdjEtNzl7XU3wWYRa3AceUKRCO6w1GM6f5EY0Ypo1lIk+gNBa+bt38kulqyJWxv7f6W4wDC/gih0hoslJPuC8s+J7e4Df7k43X1L/jsdxt0xZTX8dfHlN8="; + + String rpLogoutRequestId = "LRd49fb45a-e8a7-43ac-b8ac-d8a7432fc9b2"; + + String rpLogoutRequestRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315"; + + String rpLogoutRequestSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q=="; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + private MockFilterChain filterChain; + + @Before + public void setup() { + this.request = new MockHttpServletRequest("POST", ""); + this.request.setServletPath("/login/saml2/sso/test-rp"); + this.response = new MockHttpServletResponse(); + this.filterChain = new MockFilterChain(); + } + + @After + public void cleanup() { + if (this.context != null) { + this.context.close(); + } + reset(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf())) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/request"); + verify(Saml2LogoutDefaultsConfig.mockLogoutHandler).logout(any(), any(), any()); + } + + @Test + public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + } + + @Test + public void saml2LogoutWhenMissingCsrfThen403() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(post("/logout").with(authentication(this.user))).andExpect(status().isForbidden()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.user)).with(csrf())) + .andExpect(status().isOk()).andReturn(); + assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?"); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutWhenPutOrDeleteThen404() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(put("/logout").with(authentication(this.user)).with(csrf())).andExpect(status().isNotFound()); + this.mvc.perform(delete("/logout").with(authentication(this.user)).with(csrf())) + .andExpect(status().isNotFound()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutWhenNoRegistrationThenIllegalArgument() { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + Saml2Authentication authentication = new Saml2Authentication( + new DefaultSaml2AuthenticatedPrincipal("user", Collections.emptyMap()), "response", + AuthorityUtils.createAuthorityList("ROLE_USER"), "wrong"); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy( + () -> this.mvc.perform(post("/logout").with(authentication(authentication)).with(csrf())).andReturn()); + } + + @Test + public void saml2LogoutWhenCustomLogoutRequestResolverThenUses() throws Exception { + this.spring.register(Saml2LogoutComponentsConfig.class).autowire(); + this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf())); + verify(Saml2LogoutComponentsConfig.logoutRequestResolver).resolveLogoutRequest(any(), any()); + } + + @Test + public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + MvcResult result = this.mvc + .perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", this.apLogoutRequestSignature).with(authentication(this.user))) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/response"); + verify(Saml2LogoutDefaultsConfig.mockLogoutHandler).logout(any(), any(), any()); + } + + @Test + public void saml2LogoutRequestWhenNoRegistrationThenIllegalArgument() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .param("Signature", this.apLogoutRequestSignature)).andReturn()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutRequestWhenNoSamlRequestThen404() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(get("/logout/saml2/slo").with(authentication(this.user))).andExpect(status().isNotFound()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutRequestWhenInvalidSamlRequestThenException() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + assertThatExceptionOfType(Saml2Exception.class) + .isThrownBy(() -> this.mvc + .perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState) + .param("SigAlg", this.apLogoutRequestSigAlg).with(authentication(this.user))) + .andReturn()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutRequestWhenCustomLogoutRequestHandlerThenUses() throws Exception { + this.spring.register(Saml2LogoutComponentsConfig.class).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration); + logoutRequest.setIssueInstant(Instant.now()); + willAnswer((invocation) -> { + HttpServletRequest request = (HttpServletRequest) invocation.getArguments()[0]; + request.setAttribute(LogoutRequest.class.getName(), logoutRequest); + return null; + }).given(Saml2LogoutComponentsConfig.logoutRequestHandler).logout(any(), any(), any()); + Saml2LogoutResponseBuilder partial = mock(Saml2LogoutResponseBuilder.class, RETURNS_SELF); + given(partial.logoutResponse()) + .willReturn(Saml2LogoutResponse.withRelyingPartyRegistration(registration).build()); + willReturn(partial).given(Saml2LogoutComponentsConfig.logoutResponseResolver).resolveLogoutResponse(any(), + any()); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", "samlRequest")).andReturn(); + verify(Saml2LogoutComponentsConfig.logoutRequestHandler).logout(any(), any(), any()); + verify(Saml2LogoutComponentsConfig.logoutResponseResolver).resolveLogoutResponse(any(), any()); + } + + @Test + public void saml2LogoutResponseWhenDefaultsThenRedirectsAndDoesNotLogout() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response); + this.request.setParameter("RelayState", logoutRequest.getRelayState()); + assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNotNull(); + this.mvc.perform(get("/logout/saml2/slo").session(((MockHttpSession) this.request.getSession())) + .param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState) + .param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature)) + .andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout")); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull(); + } + + @Test + public void saml2LogoutResponseWhenNoMatchingLogoutRequestThenSaml2Exception() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.mvc.perform(get("/logout/saml2/slo") + .param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState) + .param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature))); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutResponseWhenNoSamlResponseThenEntryPoint() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(get("/logout/saml2/slo")).andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutResponseWhenInvalidSamlResponseThenException() { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response); + assertThatExceptionOfType(Saml2Exception.class).isThrownBy( + () -> this.mvc.perform(get("/logout/saml2/slo").session((MockHttpSession) this.request.getSession()) + .param("SAMLResponse", this.apLogoutRequest).param("RelayState", this.apLogoutRequestRelayState) + .param("SigAlg", this.apLogoutRequestSigAlg)).andReturn()); + verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler); + } + + @Test + public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception { + this.spring.register(Saml2LogoutComponentsConfig.class).autowire(); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLResponse", "samlResponse")).andReturn(); + verify(Saml2LogoutComponentsConfig.logoutResponseHandler).logout(any(), any(), any()); + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutDefaultsConfig { + + static final LogoutHandler mockLogoutHandler = mock(LogoutHandler.class); + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + http.authorizeRequests((authorize) -> authorize.anyRequest().authenticated()).saml2Login(withDefaults()) + .saml2Logout((logout) -> logout.addLogoutHandler(mockLogoutHandler)); + return http.build(); + } + + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutComponentsConfig { + + static final Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class); + static final LogoutHandler logoutRequestHandler = mock(LogoutHandler.class); + static final Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class); + static final LogoutHandler logoutResponseHandler = mock(LogoutHandler.class); + static final Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class); + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + http.authorizeRequests((authorize) -> authorize.anyRequest().authenticated()).saml2Login(withDefaults()) + .saml2Logout((logout) -> logout + .logoutRequest((request) -> request.logoutRequestRepository(logoutRequestRepository) + .logoutRequestHandler(logoutRequestHandler) + .logoutRequestResolver(logoutRequestResolver)) + .logoutResponse((response) -> response.logoutResponseHandler(logoutResponseHandler) + .logoutResponseResolver(logoutResponseResolver))); + return http.build(); + } + + } + + static class Saml2LoginConfigBeans { + + @Bean + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() { + Saml2X509Credential signing = TestSaml2X509Credentials.assertingPartySigningCredential(); + Saml2X509Credential verification = TestSaml2X509Credentials.relyingPartyVerifyingCredential(); + RelyingPartyRegistration.Builder withCreds = TestRelyingPartyRegistrations.noCredentials() + .signingX509Credentials(credential(signing)) + .assertingPartyDetails((party) -> party.verificationX509Credentials(credential(verification))); + RelyingPartyRegistration registration = withCreds.build(); + RelyingPartyRegistration ap = withCreds.registrationId("ap").entityId("ap-entity-id") + .assertingPartyDetails((party) -> party + .singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request") + .singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response")) + .build(); + + return new InMemoryRelyingPartyRegistrationRepository(ap, registration); + } + + private Consumer> credential(Saml2X509Credential credential) { + return (credentials) -> credentials.add(credential); + } + + } + +} diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc index 4a23a6f127..9ca850319a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc @@ -1074,9 +1074,7 @@ 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 -==== RP-Initiated Single Logout - -Given those, then for RP-initiated Single Logout, you can begin from the initial minimal example and add the following configuration: +You can begin from the initial minimal example and add the following configuration: [source,java] ---- @@ -1105,28 +1103,15 @@ SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository re .anyRequest().authenticated() ) .saml2Login(withDefaults()) - .logout((logout) -> logout - .logoutUrl("/saml2/logout") - .logoutSuccessHandler(successHandler)) - .addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class); + .saml2Logout(withDefaults()); <2> return http.build(); } - -private LogoutSuccessHandler logoutRequestSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <2> - OpenSaml4LogoutRequestResolver logoutRequestResolver = new OpenSaml4LogoutRequestResolver(registrationResolver); - return new Saml2LogoutRequestSuccessHandler(logoutRequestResolver); -} - -private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3> - return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver); -} ---- <1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <> -<2> - Second, supply a `LogoutSuccessHandler` for initiating Single Logout, sending a `saml2:LogoutRequest` to the asserting party -<3> - Third, supply the `LogoutHandler` s needed to handle the `saml2:LogoutResponse` s sent from the asserting party. +<2> - Second, indicate that your application wants to use SAML SLO to logout the end user -==== Runtime Expectations for RP-Initiated +==== Runtime Expectations 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: @@ -1137,63 +1122,6 @@ Your application will then do the following: 4. Deserialize, verify, and process the `` sent by the asserting party 5. Redirect to any configured successful logout endpoint -[TIP] -If your asserting party does not send `` s when logout is complete, the asserting party can still send a `POST /saml2/logout` and then there is no need to configure the `Saml2LogoutResponseHandler`. - -==== AP-Initiated Single Logout - -Instead of RP-initiated Single Logout, you can again begin from the initial minimal example and add the following configuration to achieve AP-initiated Single Logout: - -[source,java] ----- -@Value("${private.key}") RSAPrivateKey key; -@Value("${public.certificate}") X509Certificate certificate; - -@Bean -RelyingPartyRegistrationRepository registrations() { - RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations - .fromMetadataLocation("https://ap.example.org/metadata") - .registrationId("id") - .signingX509Credentials((signing) -> signing.add(Saml2X509Credential.signing(key, certificate))) <1> - .build(); - return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration); -} - -@Bean -SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { - RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations); - LogoutHandler logoutRequestHandler = logoutRequestHandler(registrationResolver); - LogoutSuccessHandler logoutResponseSuccessHandler = logoutResponseSuccessHandler(registrationResolver); - - http - .authorizeRequests((authorize) -> authorize - .anyRequest().authenticated() - ) - .saml2Login(withDefaults()) - .addFilterBefore(new Saml2LogoutRequestFilter(logoutResponseSuccessHandler, logoutRequestHandler), CsrfFilter.class); - - return http.build(); -} - -private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2> - return new CompositeLogoutHandler( - new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver), - new SecurityContextLogoutHandler(), - new LogoutSuccessEventPublishingLogoutHandler()); -} - -private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3> - OpenSaml4LogoutResponseResolver logoutResponseResolver = new OpenSaml4LogoutResponseResolver(registrationResolver); - return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver); -} ----- -<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <> -<2> - Second, supply the `LogoutHandler` needed to handle the `saml2:LogoutRequest` s sent from the asserting party. -<3> - Third, supply a `LogoutSuccessHandler` for completing Single Logout, sending a `saml2:LogoutResponse` to the asserting party - -==== Runtime Expectations for AP-Initiated - -Given the above configuration, an asserting party can send a `POST /logout/saml2` to your application that includes a `` Also, your application can participate in an AP-initated logout when the asserting party sends a `` to `/logout/saml2/slo`: 1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `` sent by the asserting party @@ -1201,12 +1129,6 @@ Also, your application can participate in an AP-initated logout when the asserti 3. Create, sign, and serialize a `` based on the <> associated with the just logged-out user 4. Send a redirect or post to the asserting party based on the <> -[TIP] -If your asserting party does not expect you do send a `` s when logout is complete, you may not need to configure a `LogoutSuccessHandler` - -[NOTE] -In the event that you need to support both logout flows, you can combine the above to configurations. - === Configuring Logout Endpoints There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes: @@ -1223,11 +1145,12 @@ To reduce changes in configuration for the asserting party, you can configure th [source,java] ---- -Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler); -filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET")); +RequestMatcher slo = new AntPathRequestMatcher("/SLOService.saml2", "GET"); http - // ... - .addFilterBefore(filter, CsrfFilter.class); + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request.logoutRequestMatcher(slo)) + .logoutResponse((response) -> response.logoutRequestMatcher(slo)) + ); ---- === Customizing `` Resolution @@ -1245,22 +1168,40 @@ To add other values, you can use delegation, like so: [source,java] ---- -OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver); -return (request, response, authentication) -> { - OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1> - builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2> - builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now())); - return builder.logoutRequest(); <3> -}; +@Component +public class MyOpenSamlLogoutRequestResolver implements Saml2LogoutRequestResolver { + private final OpenSaml3LogoutRequestResolver logoutRequestResolver; + + public MyOpenSamlLogoutRequestResolver(RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + this.logoutRequestResolver = new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver); + } + + @Override + public OpenSamlLogoutRequestBuilder resolveLogoutRequest(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String name = ((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute"); + String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; + return logoutRequestResolver.resolveLogoutRequest(request, authentication) <1> + .name(name) <2> + .logoutRequest((logoutRequest) -> logoutRequest.getNameID().setFormat(format)); + } +} ---- <1> - Spring Security applies default values to a `` <2> - Your application specifies customizations -<3> - You complete the invocation by calling `request()` -[NOTE] -Support for OpenSAML 4 is coming. -In anticipation of that, `OpenSamlLogoutRequestResolver` does not add an `IssueInstant`. -Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it. +Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestResolver(myOpenSamlLogoutRequestResolver) + ) + ); +---- === Customizing `` Resolution @@ -1277,24 +1218,42 @@ To add other values, you can use delegation, like so: [source,java] ---- -OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver); -return (request, response, authentication) -> { - OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1> - if (checkOtherPrevailingConditions()) { - builder.status(StatusCode.PARTIAL_LOGOUT); <2> +@Component +public class MyOpenSamlLogoutResponseResolver implements Saml2LogoutRequestResolver { + private final OpenSaml3LogoutResponseResolver logoutRequestResolver; + + public MyOpenSamlLogoutResponseResolver(RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + this.logoutResponseResolver = new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + } + + @Override + public OpenSamlLogoutResponseBuilder resolveLogoutResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String name = ((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute"); + String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; + OpenSamlLogoutResponseBuilder builder = logoutResponseResolver.resolveLogoutRequest(request, authentication); <1> + if (checkOtherPrevailingConditions()) { + builder.status(StatusCode.PARTIAL_LOGOUT); <2> + } + return builder; } - builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now())); - return builder.logoutResponse(); <3> -}; +} ---- <1> - Spring Security applies default values to a `` <2> - Your application specifies customizations -<3> - You complete the invocation by calling `response()` -[NOTE] -Support for OpenSAML 4 is coming. -In anticipation of that, `OpenSamlLogoutResponseResolver` does not add an `IssueInstant`. -Once OpenSAML 4 support is added, the default will be able to appropriate negotiate that datatype change, meaning you will no longer have to set it. +Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestResolver(myOpenSamlLogoutRequestResolver) + ) + ); +---- === Customizing `` Validation @@ -1303,16 +1262,37 @@ At this point, the validation is minimal, so you may be able to first delegate t [source,java] ---- -LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { - OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver); - return (request, response, authentication) -> { +@Component +public class MyOpenSamlLogoutRequestHandler implements LogoutHandler { + private final Saml2LogoutRequestHandler delegate; + + public MyOpenSamlLogoutRequestHandler(RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + this.delegate = new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver); + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name LogoutRequest logoutRequest = // ... parse using OpenSAML // perform custom validation - } + } } ---- +Then, you can supply your custom `LogoutHandler` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestHandler(myOpenSamlLogoutRequestHandler) + ) + ); +---- + === Customizing `` Validation To customize validation, you can implement your own `LogoutHandler`. @@ -1320,12 +1300,49 @@ At this point, the validation is minimal, so you may be able to first delegate t [source,java] ---- -LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { - OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver); - return (request, response, authentication) -> { +@Component +public class MyOpenSamlLogoutResponseHandler implements LogoutHandler { + private final Saml2LogoutResponseHandler delegate; + + public MyOpenSamlLogoutResponseHandler(RelyingPartyRegistrationRepository registrations) { + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = + new DefaultRelyingPartyRegistrationResolver(registrations); + this.delegate = new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver); + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status LogoutResponse logoutResponse = // ... parse using OpenSAML // perform custom validation - } + } } ---- + +Then, you can supply your custom `LogoutHandler` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutResponse((response) -> response + .logoutResponseHandler(myOpenSamlLogoutResponseHandler) + ) + ); +---- + +=== 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] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestRepository(myCustomLogoutRequestRepository) + ) + ); +----