Add Saml2LogoutConfigurer

Closes gh-9497
This commit is contained in:
Josh Cummings 2021-03-25 10:44:26 -06:00
parent c63d618b26
commit 4f06fc6ed1
7 changed files with 1317 additions and 159 deletions

View File

@ -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<Defaul
return HttpSecurity.this;
}
/**
* Configures logout support for an SAML 2.0 Relying Party. <br>
* <br>
*
* Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
* documented in the
* <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
* Core, Profiles and Bindings</a> specifications. <br>
* <br>
*
* 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}. <br>
* <br>
*
* {@link RelyingPartyRegistration}(s) are composed within a
* {@link RelyingPartyRegistrationRepository}, which is <b>required</b> and must be
* registered with the {@link ApplicationContext} or configured via
* {@link #saml2Login(Customizer)}.<br>
* <br>
*
* The default configuration provides an auto-generated logout endpoint at
* <code>&quot;/logout&quot;</code> and redirects to <code>/login?logout</code> when
* logout completes. <br>
* <br>
*
* <p>
* <h2>Example Configuration</h2>
*
* The following example shows the minimal configuration required, using a
* hypothetical asserting party.
*
* <pre>
* &#064;EnableWebSecurity
* &#064;Configuration
* public class Saml2LogoutSecurityConfig {
* &#064;Bean
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
* http
* .authorizeRequests((authorize) -> authorize
* .anyRequest().authenticated()
* )
* .saml2Login(withDefaults())
* .saml2Logout(withDefaults());
* return http.build();
* }
*
* &#064;Bean
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
* RelyingPartyRegistration registration = RelyingPartyRegistrations
* .withMetadataLocation("https://ap.example.org/metadata")
* .registrationId("simple")
* .build();
* return new InMemoryRelyingPartyRegistrationRepository(registration);
* }
* }
* </pre>
*
* <p>
* @return the {@link HttpSecurity} for further customizations
* @throws Exception
* @since 5.6
*/
public HttpSecurity saml2Logout(Customizer<Saml2LogoutConfigurer<HttpSecurity>> saml2LogoutCustomizer)
throws Exception {
saml2LogoutCustomizer.customize(getOrApply(new Saml2LogoutConfigurer<>(getContext())));
return HttpSecurity.this;
}
/**
* Configures logout support for an SAML 2.0 Relying Party. <br>
* <br>
*
* Implements the <b>Single Logout Profile, using POST and REDIRECT bindings</b>, as
* documented in the
* <a target="_blank" href="https://docs.oasis-open.org/security/saml/">SAML V2.0
* Core, Profiles and Bindings</a> specifications. <br>
* <br>
*
* 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}. <br>
* <br>
*
* {@link RelyingPartyRegistration}(s) are composed within a
* {@link RelyingPartyRegistrationRepository}, which is <b>required</b> and must be
* registered with the {@link ApplicationContext} or configured via
* {@link #saml2Login()}.<br>
* <br>
*
* The default configuration provides an auto-generated logout endpoint at
* <code>&quot;/logout&quot;</code> and redirects to <code>/login?logout</code> when
* logout completes. <br>
* <br>
*
* <p>
* <h2>Example Configuration</h2>
*
* The following example shows the minimal configuration required, using a
* hypothetical asserting party.
*
* <pre>
* &#064;EnableWebSecurity
* &#064;Configuration
* public class Saml2LogoutSecurityConfig {
* &#064;Bean
* public SecurityFilterChain web(HttpSecurity http) throws Exception {
* http
* .authorizeRequests()
* .anyRequest().authenticated()
* .and()
* .saml2Login()
* .and()
* .saml2Logout();
* return http.build();
* }
*
* &#064;Bean
* public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
* RelyingPartyRegistration registration = RelyingPartyRegistrations
* .withMetadataLocation("https://ap.example.org/metadata")
* .registrationId("simple")
* .build();
* return new InMemoryRelyingPartyRegistrationRepository(registration);
* }
* }
* </pre>
*
* <p>
* @return the {@link Saml2LoginConfigurer} for further customizations
* @throws Exception
* @since 5.6
*/
public Saml2LogoutConfigurer<HttpSecurity> saml2Logout() throws Exception {
return getOrApply(new Saml2LogoutConfigurer<>(getContext()));
}
/**
* Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
* Provider. <br>

View File

@ -250,10 +250,11 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
* {@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<H extends HttpSecurityBuilder<H>>
* Gets the {@link LogoutHandler} instances that will be used.
* @return the {@link LogoutHandler} instances. Cannot be null.
*/
List<LogoutHandler> getLogoutHandlers() {
public List<LogoutHandler> getLogoutHandlers() {
return this.logoutHandlers;
}

View File

@ -205,9 +205,7 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
@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<B extends HttpSecurityBuilder<B>>
}
}
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));

View File

@ -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.
*
* <h2>Security Filters</h2>
*
* The following Filters are populated
*
* <ul>
* <li>{@link LogoutFilter}</li>
* <li>{@link Saml2LogoutRequestFilter}</li>
* <li>{@link Saml2LogoutResponseFilter}</li>
* </ul>
*
* <p>
* The following configuration options are available:
*
* <ul>
* <li>{@link #logoutUrl} - The URL to to process SAML 2.0 Logout</li>
* <li>{@link LogoutRequestConfigurer#logoutRequestValidator} - The
* {@link AuthenticationManager} for authenticating SAML 2.0 Logout Requests</li>
* <li>{@link LogoutRequestConfigurer#logoutRequestResolver} - The
* {@link Saml2LogoutRequestResolver} for creating SAML 2.0 Logout Requests</li>
* <li>{@link LogoutRequestConfigurer#logoutRequestRepository} - The
* {@link Saml2LogoutRequestRepository} for storing SAML 2.0 Logout Requests</li>
* <li>{@link LogoutResponseConfigurer#logoutResponseValidator} - The
* {@link AuthenticationManager} for authenticating SAML 2.0 Logout Responses</li>
* <li>{@link LogoutResponseConfigurer#logoutResponseResolver} - The
* {@link Saml2LogoutResponseResolver} for creating SAML 2.0 Logout Responses</li>
* </ul>
*
* <h2>Shared Objects Created</h2>
*
* No shared Objects are created
*
* <h2>Shared Objects Used</h2>
*
* Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutConfigurer
*/
public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
extends AbstractHttpConfigurer<Saml2LogoutConfigurer<H>, H> {
private ApplicationContext context;
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private String logoutUrl = "/logout";
private List<LogoutHandler> 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.
*
* <p>
* 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<H> 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<H> 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<H> logoutRequest(
Customizer<LogoutRequestConfigurer> 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<H> logoutResponse(
Customizer<LogoutResponseConfigurer> logoutResponseConfigurerCustomizer) {
logoutResponseConfigurerCustomizer.customize(this.logoutResponseConfigurer);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public void configure(H http) throws Exception {
LogoutConfigurer<H> 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<H> 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> C getBeanOrNull(Class<C> 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
*
* <p>
* 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<H> 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
*
* <p>
* 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<H> 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<String> 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) {
}
}
}

View File

@ -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<LogoutFilter> 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<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
ObjectPostProcessor<Object> objectPostProcessor = spy(ReflectingObjectPostProcessor.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
@ -372,8 +374,8 @@ public class LogoutConfigurerTests {
}
@Bean
static ObjectPostProcessor<Object> objectPostProcessor() {
return objectPostProcessor;
ObjectPostProcessor<Object> objectPostProcessor() {
return this.objectPostProcessor;
}
}

View File

@ -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> T getBean(Class<T> 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<Collection<Saml2X509Credential>> credential(Saml2X509Credential credential) {
return (credentials) -> credentials.add(credential);
}
}
}

View File

@ -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 <<servlet-saml2login-rpr-duplicated,multiple instances>>
<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 `<saml2:LogoutResponse>` sent by the asserting party
5. Redirect to any configured successful logout endpoint
[TIP]
If your asserting party does not send `<saml2:LogoutResponse>` 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 <<servlet-saml2login-rpr-duplicated,multiple instances>>
<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 `<saml2:LogoutRequest>`
Also, your application can participate in an AP-initated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
Also, your application can participate in an AP-initiated logout when the asserting party sends a `<saml2:LogoutRequest>` to `/logout/saml2/slo`:
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
2. Logout the user and invalidate the session
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
4. Send a redirect or post to the asserting party based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>>
[TIP]
If your asserting party does not expect you do send a `<saml2:LogoutResponse>` 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 `<saml2:LogoutRequest>`
* AP-initiated logout, which allows an asserting party to send a `<saml2:LogoutRequest>` to the application
* AP logout response, which allows an asserting party to send a `<saml2:LogoutResponse>` in response to the RP-initiated `<saml2:LogoutRequest>`
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 `<saml2:LogoutRequest>` 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 `<saml2:LogoutRequest>`
<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 `<saml2:LogoutResponse>` 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 `<saml2:LogoutResponse>`
<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 `<saml2:LogoutRequest>` 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 `<saml2:LogoutRequest>` 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 `<saml2:LogoutResponse>` 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 `<saml2:LogoutResponse>` 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 `<saml2:LogoutRequest>` storage
When your application sends a `<saml2:LogoutRequest>`, the value is stored in the session so that the `RelayState` parameter and the `InResponseTo` attribute in the `<saml2:LogoutResponse>` 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)
)
);
----