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 42a1f60f58..b4d35729fb 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 @@ -72,6 +72,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; @@ -2209,6 +2210,143 @@ 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 + * {@link #saml2Login(Customizer)}.
+ *
+ * + * The default configuration provides an auto-generated logout endpoint at + * "/logout" and redirects to /login?logout when + * logout completes.
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using a + * hypothetical 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 HttpSecurity} for further customizations + * @throws Exception + * @since 5.6 + */ + 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 + * {@link #saml2Login()}.
+ *
+ * + * The default configuration provides an auto-generated logout endpoint at + * "/logout" and redirects to /login?logout when + * logout completes.
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using a + * hypothetical asserting party. + * + *
+	 *	@EnableWebSecurity
+	 *	@Configuration
+	 *	public class Saml2LogoutSecurityConfig {
+	 *		@Bean
+	 *		public SecurityFilterChain web(HttpSecurity http) throws Exception {
+	 *			http
+	 *				.authorizeRequests()
+	 *					.anyRequest().authenticated()
+	 *					.and()
+	 *				.saml2Login()
+	 *					.and()
+	 *				.saml2Logout();
+	 *			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.6 + */ + 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/LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java index a86d8339dc..be65179302 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java @@ -250,10 +250,11 @@ public final class LogoutConfigurer> * {@link SimpleUrlLogoutSuccessHandler} using the {@link #logoutSuccessUrl(String)}. * @return the {@link LogoutSuccessHandler} to use */ - private LogoutSuccessHandler getLogoutSuccessHandler() { + public LogoutSuccessHandler getLogoutSuccessHandler() { LogoutSuccessHandler handler = this.logoutSuccessHandler; if (handler == null) { handler = createDefaultSuccessHandler(); + this.logoutSuccessHandler = handler; } return handler; } @@ -312,7 +313,7 @@ public final class LogoutConfigurer> * Gets the {@link LogoutHandler} instances that will be used. * @return the {@link LogoutHandler} instances. Cannot be null. */ - List getLogoutHandlers() { + public List getLogoutHandlers() { return this.logoutHandlers; } 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 9860e8040b..be18c5c5b2 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 @@ -205,9 +205,7 @@ public final class Saml2LoginConfigurer> @Override public void init(B http) throws Exception { registerDefaultCsrfOverride(http); - if (this.relyingPartyRegistrationRepository == null) { - this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); - } + relyingPartyRegistrationRepository(http); this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http), this.loginProcessingUrl); setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter); @@ -257,6 +255,13 @@ public final class Saml2LoginConfigurer> } } + RelyingPartyRegistrationRepository relyingPartyRegistrationRepository(B http) { + if (this.relyingPartyRegistrationRepository == null) { + this.relyingPartyRegistrationRepository = getSharedOrBean(http, RelyingPartyRegistrationRepository.class); + } + return this.relyingPartyRegistrationRepository; + } + private void setAuthenticationRequestRepository(B http, Saml2WebSsoAuthenticationFilter saml2WebSsoAuthenticationFilter) { saml2WebSsoAuthenticationFilter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); 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..113d945c1d --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -0,0 +1,523 @@ +/* + * 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 java.util.Objects; +import java.util.function.Predicate; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.opensaml.core.Version; + +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.AuthenticationManager; +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.LogoutConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSamlLogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; +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.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.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.Saml2LogoutResponseFilter; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler; +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.CsrfFilter; +import org.springframework.security.web.csrf.CsrfLogoutHandler; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * 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 to process SAML 2.0 Logout
  • + *
  • {@link LogoutRequestConfigurer#logoutRequestValidator} - The + * {@link AuthenticationManager} for authenticating 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#logoutResponseValidator} - The + * {@link AuthenticationManager} for authenticating 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.6 + * @see Saml2LogoutConfigurer + */ +public final class Saml2LogoutConfigurer> + extends AbstractHttpConfigurer, H> { + + private ApplicationContext context; + + private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; + + private String logoutUrl = "/logout"; + + private List logoutHandlers = new ArrayList<>(); + + private LogoutSuccessHandler logoutSuccessHandler; + + private LogoutRequestConfigurer logoutRequestConfigurer; + + private LogoutResponseConfigurer logoutResponseConfigurer; + + /** + * Creates a new instance + * @see HttpSecurity#logout() + */ + public Saml2LogoutConfigurer(ApplicationContext context) { + this.context = context; + this.logoutHandlers.add(new SecurityContextLogoutHandler()); + this.logoutHandlers.add(new LogoutSuccessEventPublishingLogoutHandler()); + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl("/login?logout"); + this.logoutSuccessHandler = logoutSuccessHandler; + this.logoutRequestConfigurer = new LogoutRequestConfigurer(); + this.logoutResponseConfigurer = new LogoutResponseConfigurer(); + } + + /** + * The URL by which the relying or asserting party can trigger logout. + * + *

+ * The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party + * triggers logout based on what is specified by + * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * @param logoutUrl the URL that will invoke logout + * @return the {@link LogoutConfigurer} for further customizations + * @see LogoutConfigurer#logoutUrl(String) + * @see HttpSecurity#csrf() + */ + public Saml2LogoutConfigurer logoutUrl(String logoutUrl) { + this.logoutUrl = logoutUrl; + return this; + } + + /** + * Sets the {@link 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 Saml2LogoutConfigurer} for further customizations + */ + 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 { + LogoutConfigurer logout = http.getConfigurer(LogoutConfigurer.class); + if (logout != null) { + this.logoutHandlers = logout.getLogoutHandlers(); + this.logoutSuccessHandler = logout.getLogoutSuccessHandler(); + } + RelyingPartyRegistrationResolver registrations = relyingPartyRegistrationResolver(http); + http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class); + http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class); + http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class); + } + + private RelyingPartyRegistrationResolver relyingPartyRegistrationResolver(H http) { + RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http); + return new DefaultRelyingPartyRegistrationResolver(registrations); + } + + private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) { + if (this.relyingPartyRegistrationRepository != null) { + return this.relyingPartyRegistrationRepository; + } + Saml2LoginConfigurer login = http.getConfigurer(Saml2LoginConfigurer.class); + if (login != null) { + this.relyingPartyRegistrationRepository = login.relyingPartyRegistrationRepository(http); + } + else { + this.relyingPartyRegistrationRepository = getBeanOrNull(RelyingPartyRegistrationRepository.class); + } + return this.relyingPartyRegistrationRepository; + } + + private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter( + RelyingPartyRegistrationResolver registrations) { + LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); + Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations); + Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(registrations, + this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers); + filter.setLogoutRequestMatcher(createLogoutRequestMatcher()); + return filter; + } + + private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter( + RelyingPartyRegistrationResolver registrations) { + Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations, + this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler); + logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher()); + logoutResponseFilter.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository); + return logoutResponseFilter; + } + + private LogoutFilter createRelyingPartyLogoutFilter(RelyingPartyRegistrationResolver registrations) { + LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]); + Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler( + registrations); + LogoutFilter logoutFilter = new LogoutFilter(logoutRequestSuccessHandler, logoutHandlers); + logoutFilter.setLogoutRequestMatcher(createLogoutMatcher()); + return logoutFilter; + } + + private RequestMatcher createLogoutMatcher() { + RequestMatcher logout = new AntPathRequestMatcher(this.logoutUrl, "POST"); + RequestMatcher saml2 = new Saml2RequestMatcher(); + return new AndRequestMatcher(logout, saml2); + } + + private RequestMatcher createLogoutRequestMatcher() { + RequestMatcher logout = new AntPathRequestMatcher(this.logoutRequestConfigurer.logoutUrl); + RequestMatcher samlRequest = new ParameterRequestMatcher("SAMLRequest"); + return new AndRequestMatcher(logout, samlRequest); + } + + private RequestMatcher createLogoutResponseMatcher() { + RequestMatcher logout = new AntPathRequestMatcher(this.logoutResponseConfigurer.logoutUrl); + RequestMatcher samlResponse = new ParameterRequestMatcher("SAMLResponse"); + return new AndRequestMatcher(logout, samlResponse); + } + + private Saml2RelyingPartyInitiatedLogoutSuccessHandler createSaml2LogoutRequestSuccessHandler( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer + .logoutRequestResolver(relyingPartyRegistrationResolver); + return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver); + } + + private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + return this.logoutResponseConfigurer.logoutResponseResolver(relyingPartyRegistrationResolver); + } + + 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); + } + + private String version() { + String version = Version.getVersion(); + if (version != null) { + return version; + } + return Version.class.getModule().getDescriptor().version().map(Object::toString) + .orElseThrow(() -> new IllegalStateException("cannot determine OpenSAML version")); + } + + /** + * A configurer for SAML 2.0 LogoutRequest components + */ + public final class LogoutRequestConfigurer { + + private String logoutUrl = "/logout/saml2/slo"; + + private Saml2LogoutRequestValidator logoutRequestValidator; + + private Saml2LogoutRequestResolver logoutRequestResolver; + + private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository(); + + LogoutRequestConfigurer() { + } + + /** + * The URL by which the asserting party can send a SAML 2.0 Logout Request + * + *

+ * The Asserting Party should use whatever HTTP method specified in + * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * @param logoutUrl the URL that will receive the SAML 2.0 Logout Request + * @return the {@link LogoutRequestConfigurer} for further customizations + * @see Saml2LogoutConfigurer#logoutUrl(String) + */ + public LogoutRequestConfigurer logoutUrl(String logoutUrl) { + this.logoutUrl = logoutUrl; + return this; + } + + /** + * Use this {@link LogoutHandler} for processing a logout request from the + * asserting party + * @param authenticator the {@link Saml2LogoutRequestValidator} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutRequestConfigurer logoutRequestValidator(Saml2LogoutRequestValidator authenticator) { + this.logoutRequestValidator = authenticator; + 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.logoutRequestResolver = 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 Saml2LogoutRequestValidator logoutRequestValidator() { + if (this.logoutRequestValidator == null) { + return new OpenSamlLogoutRequestValidator(); + } + return this.logoutRequestValidator; + } + + private Saml2LogoutRequestResolver logoutRequestResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutRequestResolver != null) { + return this.logoutRequestResolver; + } + if (version().startsWith("4")) { + return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver); + } + return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver); + } + + } + + public final class LogoutResponseConfigurer { + + private String logoutUrl = "/logout/saml2/slo"; + + private Saml2LogoutResponseValidator logoutResponseValidator; + + private Saml2LogoutResponseResolver logoutResponseResolver; + + LogoutResponseConfigurer() { + } + + /** + * The URL by which the asserting party can send a SAML 2.0 Logout Response + * + *

+ * The Asserting Party should use whatever HTTP method specified in + * {@link RelyingPartyRegistration#getSingleLogoutServiceBinding()}. + * @param logoutUrl the URL that will receive the SAML 2.0 Logout Response + * @return the {@link LogoutResponseConfigurer} for further customizations + * @see Saml2LogoutConfigurer#logoutUrl(String) + */ + public LogoutResponseConfigurer logoutUrl(String logoutUrl) { + this.logoutUrl = logoutUrl; + return this; + } + + /** + * Use this {@link LogoutHandler} for processing a logout response from the + * asserting party + * @param authenticator the {@link AuthenticationManager} to use + * @return the {@link LogoutRequestConfigurer} for further customizations + */ + public LogoutResponseConfigurer logoutResponseValidator(Saml2LogoutResponseValidator authenticator) { + this.logoutResponseValidator = authenticator; + 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.logoutResponseResolver = logoutResponseResolver; + return this; + } + + public Saml2LogoutConfigurer and() { + return Saml2LogoutConfigurer.this; + } + + private Saml2LogoutResponseValidator logoutResponseValidator() { + if (this.logoutResponseValidator == null) { + return new OpenSamlLogoutResponseValidator(); + } + return this.logoutResponseValidator; + } + + private Saml2LogoutResponseResolver logoutResponseResolver( + RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) { + if (this.logoutResponseResolver == null) { + if (version().startsWith("4")) { + return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver); + } + return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + } + return this.logoutResponseResolver; + } + + } + + private static class Saml2RequestMatcher implements RequestMatcher { + + @Override + public boolean matches(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null) { + return false; + } + return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal; + } + + } + + private static class ParameterRequestMatcher implements RequestMatcher { + + Predicate test = Objects::nonNull; + + String name; + + ParameterRequestMatcher(String name) { + this.name = name; + } + + @Override + public boolean matches(HttpServletRequest request) { + return this.test.test(request.getParameter(this.name)); + } + + } + + private static class NoopLogoutHandler implements LogoutHandler { + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + } + + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index 4fef78e9a1..1433e0ec1b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -97,7 +97,9 @@ public class LogoutConfigurerTests { @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); - verify(ObjectPostProcessorConfig.objectPostProcessor).postProcess(any(LogoutFilter.class)); + ObjectPostProcessor objectPostProcessor = this.spring.getContext() + .getBean(ObjectPostProcessor.class); + verify(objectPostProcessor).postProcess(any(LogoutFilter.class)); } @Test @@ -361,7 +363,7 @@ public class LogoutConfigurerTests { @EnableWebSecurity static class ObjectPostProcessorConfig extends WebSecurityConfigurerAdapter { - static ObjectPostProcessor objectPostProcessor = spy(ReflectingObjectPostProcessor.class); + ObjectPostProcessor objectPostProcessor = spy(ReflectingObjectPostProcessor.class); @Override protected void configure(HttpSecurity http) throws Exception { @@ -372,8 +374,8 @@ public class LogoutConfigurerTests { } @Bean - static ObjectPostProcessor objectPostProcessor() { - return objectPostProcessor; + ObjectPostProcessor objectPostProcessor() { + return this.objectPostProcessor; } } 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..c47654bf23 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurerTests.java @@ -0,0 +1,493 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.test.SpringTestContext; +import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.saml2.core.Saml2Utils; +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.Saml2LogoutRequestValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponseValidator; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutValidatorResult; +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.Saml2MessageBinding; +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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +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.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.verifyNoInteractions; +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} + */ +@ExtendWith(SpringTestContextExtension.class) +public class Saml2LogoutConfigurerTests { + + @Autowired + private ConfigurableApplicationContext context; + + @Autowired + private RelyingPartyRegistrationRepository repository; + + private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository(); + + public final SpringTestContext spring = new SpringTestContext(this); + + @Autowired(required = false) + MockMvc mvc; + + private Saml2Authentication user; + + 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; + + @BeforeEach + public void setup() { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("registration-id"); + this.user = new Saml2Authentication(principal, "response", AuthorityUtils.createAuthorityList("ROLE_USER")); + this.request = new MockHttpServletRequest("POST", ""); + this.request.setServletPath("/login/saml2/sso/test-rp"); + this.response = new MockHttpServletResponse(); + } + + @AfterEach + public void cleanup() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void logoutWhenDefaultsAndNotSaml2LoginThenDefaultLogout() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + TestingAuthenticationToken user = new TestingAuthenticationToken("user", "password"); + MvcResult result = this.mvc.perform(post("/logout").with(authentication(user)).with(csrf())) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class); + assertThat(location).isEqualTo("/login?logout"); + verify(logoutHandler).logout(any(), any(), any()); + } + + @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"); + LogoutHandler logoutHandler = this.spring.getContext().getBean(LogoutHandler.class); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/request"); + verify(logoutHandler).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(getBean(LogoutHandler.class)); + } + + @Test + public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.user))).andExpect(status().isOk()) + .andReturn(); + assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?"); + verifyNoInteractions(getBean(LogoutHandler.class)); + } + + @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(this.spring.getContext().getBean(LogoutHandler.class)); + } + + @Test + public void saml2LogoutWhenNoRegistrationThen401() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("wrong"); + Saml2Authentication authentication = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + this.mvc.perform(post("/logout").with(authentication(authentication)).with(csrf())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void saml2LogoutWhenCsrfDisabledAndNoAuthenticationThenFinalRedirect() throws Exception { + this.spring.register(Saml2LogoutCsrfDisabledConfig.class).autowire(); + this.mvc.perform(post("/logout")); + LogoutSuccessHandler logoutSuccessHandler = this.spring.getContext().getBean(LogoutSuccessHandler.class); + verify(logoutSuccessHandler).onLogoutSuccess(any(), any(), any()); + } + + @Test + public void saml2LogoutWhenCustomLogoutRequestResolverThenUses() throws Exception { + this.spring.register(Saml2LogoutComponentsConfig.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(); + given(getBean(Saml2LogoutRequestResolver.class).resolve(any(), any())).willReturn(logoutRequest); + this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf())); + verify(getBean(Saml2LogoutRequestResolver.class)).resolve(any(), any()); + } + + @Test + public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("get"); + Saml2Authentication user = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + 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(user))) + .andExpect(status().isFound()).andReturn(); + String location = result.getResponse().getHeader("Location"); + assertThat(location).startsWith("https://ap.example.org/logout/saml2/response"); + verify(getBean(LogoutHandler.class)).logout(any(), any(), any()); + } + + @Test + public void saml2LogoutRequestWhenNoRegistrationThen400() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal("user", + Collections.emptyMap()); + principal.setRelyingPartyRegistrationId("wrong"); + Saml2Authentication user = new Saml2Authentication(principal, "response", + AuthorityUtils.createAuthorityList("ROLE_USER")); + 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(user))) + .andExpect(status().isBadRequest()); + verifyNoInteractions(getBean(LogoutHandler.class)); + } + + @Test + public void saml2LogoutRequestWhenInvalidSamlRequestThen401() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest) + .param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg) + .with(authentication(this.user))).andExpect(status().isUnauthorized()); + verifyNoInteractions(getBean(LogoutHandler.class)); + } + + @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()); + given(getBean(Saml2LogoutRequestValidator.class).validate(any())) + .willReturn(Saml2LogoutValidatorResult.success()); + Saml2LogoutResponse logoutResponse = Saml2LogoutResponse.withRelyingPartyRegistration(registration).build(); + given(getBean(Saml2LogoutResponseResolver.class).resolve(any(), any())).willReturn(logoutResponse); + this.mvc.perform(post("/logout/saml2/slo").param("SAMLRequest", "samlRequest").with(authentication(this.user))) + .andReturn(); + verify(getBean(Saml2LogoutRequestValidator.class)).validate(any()); + verify(getBean(Saml2LogoutResponseResolver.class)).resolve(any(), any()); + } + + @Test + public void saml2LogoutResponseWhenDefaultsThenRedirects() throws Exception { + this.spring.register(Saml2LogoutDefaultsConfig.class).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("get"); + 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(getBean(LogoutHandler.class)); + assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull(); + } + + @Test + public void saml2LogoutResponseWhenInvalidSamlResponseThen401() 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); + String deflatedApLogoutResponse = Saml2Utils.samlEncode( + Saml2Utils.samlInflate(Saml2Utils.samlDecode(this.apLogoutResponse)).getBytes(StandardCharsets.UTF_8)); + this.mvc.perform(post("/logout/saml2/slo").session((MockHttpSession) this.request.getSession()) + .param("SAMLResponse", deflatedApLogoutResponse).param("RelayState", this.rpLogoutRequestRelayState) + .param("SigAlg", this.apLogoutRequestSigAlg).param("Signature", this.apLogoutResponseSignature)) + .andExpect(status().reason(containsString("invalid_signature"))).andExpect(status().isUnauthorized()); + verifyNoInteractions(getBean(LogoutHandler.class)); + } + + @Test + public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception { + this.spring.register(Saml2LogoutComponentsConfig.class).autowire(); + RelyingPartyRegistration registration = this.repository.findByRegistrationId("get"); + Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration) + .samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState) + .parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build(); + given(getBean(Saml2LogoutRequestRepository.class).removeLogoutRequest(any(), any())).willReturn(logoutRequest); + given(getBean(Saml2LogoutResponseValidator.class).validate(any())) + .willReturn(Saml2LogoutValidatorResult.success()); + this.mvc.perform(get("/logout/saml2/slo").param("SAMLResponse", "samlResponse")).andReturn(); + verify(getBean(Saml2LogoutResponseValidator.class)).validate(any()); + } + + private T getBean(Class clazz) { + return this.spring.getContext().getBean(clazz); + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutDefaultsConfig { + + LogoutHandler mockLogoutHandler = mock(LogoutHandler.class); + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .logout((logout) -> logout.addLogoutHandler(this.mockLogoutHandler)) + .saml2Login(withDefaults()) + .saml2Logout(withDefaults()); + return http.build(); + // @formatter:on + } + + @Bean + LogoutHandler logoutHandler() { + return this.mockLogoutHandler; + } + + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutCsrfDisabledConfig { + + LogoutSuccessHandler mockLogoutSuccessHandler = mock(LogoutSuccessHandler.class); + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .logout((logout) -> logout.logoutSuccessHandler(this.mockLogoutSuccessHandler)) + .saml2Login(withDefaults()) + .saml2Logout(withDefaults()) + .csrf().disable(); + return http.build(); + // @formatter:on + } + + @Bean + LogoutSuccessHandler logoutSuccessHandler() { + return this.mockLogoutSuccessHandler; + } + + } + + @EnableWebSecurity + @Import(Saml2LoginConfigBeans.class) + static class Saml2LogoutComponentsConfig { + + Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class); + + Saml2LogoutRequestValidator logoutRequestValidator = mock(Saml2LogoutRequestValidator.class); + + Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class); + + Saml2LogoutResponseValidator logoutResponseValidator = mock(Saml2LogoutResponseValidator.class); + + Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class); + + @Bean + SecurityFilterChain web(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authorize) -> authorize.anyRequest().authenticated()) + .saml2Login(withDefaults()) + .saml2Logout((logout) -> logout + .logoutRequest((request) -> request + .logoutRequestRepository(this.logoutRequestRepository) + .logoutRequestValidator(this.logoutRequestValidator) + .logoutRequestResolver(this.logoutRequestResolver) + ) + .logoutResponse((response) -> response + .logoutResponseValidator(this.logoutResponseValidator) + .logoutResponseResolver(this.logoutResponseResolver) + ) + ); + return http.build(); + // @formatter:on + } + + @Bean + Saml2LogoutRequestRepository logoutRequestRepository() { + return this.logoutRequestRepository; + } + + @Bean + Saml2LogoutRequestValidator logoutRequestAuthenticator() { + return this.logoutRequestValidator; + } + + @Bean + Saml2LogoutRequestResolver logoutRequestResolver() { + return this.logoutRequestResolver; + } + + @Bean + Saml2LogoutResponseValidator logoutResponseAuthenticator() { + return this.logoutResponseValidator; + } + + @Bean + Saml2LogoutResponseResolver logoutResponseResolver() { + return this.logoutResponseResolver; + } + + } + + 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 post = withCreds.build(); + RelyingPartyRegistration get = withCreds.registrationId("get") + .singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT).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, get, post); + } + + 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 9092346b2b..b4483fa5bb 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 @@ -1,4 +1,5 @@ + [[servlet-saml2login]] == SAML 2.0 Login :figures: images/servlet/saml2 @@ -791,7 +792,7 @@ spring: okta: signing.credentials: &relying-party-credentials - private-key-location: classpath:rp.key - - certificate-location: classpath:rp.crt + certificate-location: classpath:rp.crt identityprovider: entity-id: ... azure: @@ -1639,9 +1640,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] ---- @@ -1650,48 +1649,31 @@ Given those, then for RP-initiated Single Logout, you can begin from the initial @Bean RelyingPartyRegistrationRepository registrations() { - RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations + 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(Saml2X509Credential.signing(key, certificate))) <1> + .signingX509Credentials((signing) -> signing.add(credential)) <1> .build(); - return new InMemoryRelyingPartyRegistrationRepository(relyingPartyRegistration); + return new InMemoryRelyingPartyRegistrationRepository(registration); } @Bean SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception { - RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations); - LogoutHandler logoutResponseHandler = logoutResponseHandler(registrationResolver); - LogoutSuccessHandler logoutRequestSuccessHandler = logoutRequestSuccessHandler(registrationResolver); - http .authorizeRequests((authorize) -> authorize .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: @@ -1702,86 +1684,30 @@ 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`: +Also, your application can participate in an AP-initiated 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 2. Logout the user and invalidate the session 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: -* `/logout` - the endpoint for initiating single logout with an asserting party -* `/logout/saml2/slo` - the endpoint for receiving logout requests or responses from an asserting party +There are three behaviors that can be triggered by different endpoints: +* RP-initiated logout, which allows an authenticated user to `POST` and trigger the logout process by sending the asserting party a `` +* AP-initiated logout, which allows an asserting party to send a `` to the application +* AP logout response, which allows an asserting party to send a `` in response to the RP-initiated `` -Because the user is already logged in, the `registrationId` is already known. +The first is triggered by performing normal `POST /logout` when the principal is of type `Saml2AuthenticatedPrincipal`. + +The second is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLRequest` signed by the asserting party. + +The third is triggered by POSTing to the `/logout/saml2/slo` endpoint with a `SAMLResponse` signed by the asserting party. + +Because the user is already logged in or the original Logout Request is known, the `registrationId` is already known. For this reason, `+{registrationId}+` is not part of these URLs by default. -These URLs are customizable in the DSL. +This URL is customizable in the DSL. For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`. To reduce changes in configuration for the asserting party, you can configure the filter in the DSL like so: @@ -1790,12 +1716,15 @@ To reduce changes in configuration for the asserting party, you can configure th .Java [source,java,role="primary"] ---- -Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler); -filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET")); http - // ... - .addFilterBefore(filter, CsrfFilter.class); + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2")) + .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")) + ); ---- +==== + +You should also configure these endpoints in your `RelyingPartyRegistration`. === Customizing `` Resolution @@ -1812,22 +1741,33 @@ 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> -}; +@Bean +Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) { + OpenSaml4LogoutRequestResolver logoutRequestResolver + new OpenSaml4LogoutRequestResolver(registrationResolver); + logoutRequestResolver.setParametersConsumer((parameters) -> { + String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute"); + String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"; + LogoutRequest logoutRequest = parameters.getLogoutRequest(); + NameID nameId = logoutRequest.getNameID(); + nameId.setValue(name); + nameId.setFormat(format); + }); + return logoutRequestResolver; +} ---- -<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(this.logoutRequestResolver) + ) + ); +---- === Customizing `` Resolution @@ -1844,55 +1784,111 @@ 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> - } - builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now())); - return builder.logoutResponse(); <3> -}; +@Bean +public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) { + OpenSaml4LogoutResponseResolver logoutRequestResolver = + new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver); + logoutRequestResolver.setParametersConsumer((parameters) -> { + if (checkOtherPrevailingConditions(parameters.getRequest())) { + parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT); + } + }); + return logoutRequestResolver; +} ---- -<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. - -=== Customizing `` Validation - -To customize validation, you can implement your own `LogoutHandler`. -At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so: +Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows: [source,java] ---- -LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { - OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver); - return (request, response, authentication) -> { - delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .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] +---- +@Component +public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator { + private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator(); + + @Override + public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) { + // verify signature, issuer, destination, and principal name + Saml2LogoutValidatorResult result = delegate.authenticate(authentication); + LogoutRequest logoutRequest = // ... parse using OpenSAML // perform custom validation - } + } } ---- -=== Customizing `` Validation - -To customize validation, you can implement your own `LogoutHandler`. -At this point, the validation is minimal, so you may be able to first delegate to the default `LogoutHandler` like so: +Then, you can supply your custom `Saml2LogoutRequestValidator` in the DSL as follows: [source,java] ---- -LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { - OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver); - return (request, response, authentication) -> { - delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status +http + .saml2Logout((saml2) -> saml2 + .logoutRequest((request) -> request + .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator) + ) + ); +---- + +=== 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] +---- +@Component +public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator { + private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator(); + + @Override + public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) { + // verify signature, issuer, destination, and status + Saml2LogoutValidatorResult result = delegate.authenticate(parameters); + LogoutResponse logoutResponse = // ... parse using OpenSAML // perform custom validation - } + } } ---- + +Then, you can supply your custom `Saml2LogoutResponseValidator` in the DSL as follows: + +[source,java] +---- +http + .saml2Logout((saml2) -> saml2 + .logoutResponse((response) -> response + .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator) + ) + ); +---- + +=== 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) + ) + ); +----