parent
44763345d3
commit
4e81bbe386
|
@ -64,7 +64,6 @@ 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.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
|
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.Saml2LoginConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
@ -2122,142 +2121,6 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||||
return HttpSecurity.this;
|
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
|
|
||||||
* <code>saml2Logout().relyingPartyRegistrationRepository(..)</code>. <br>
|
|
||||||
* <br>
|
|
||||||
*
|
|
||||||
* The default configuration provides an auto-generated logout endpoint at
|
|
||||||
* <code>"/saml2/logout"</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 SimpleSamlPhp
|
|
||||||
* as the asserting party.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* @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);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* @return the {@link Saml2LoginConfigurer} for further customizations
|
|
||||||
* @throws Exception
|
|
||||||
* @since 5.5
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
* <code>saml2Logout().relyingPartyRegistrationRepository(..)</code>. <br>
|
|
||||||
* <br>
|
|
||||||
*
|
|
||||||
* The default configuration provides an auto-generated logout endpoint at
|
|
||||||
* <code>"/saml2/logout"</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 SimpleSamlPhp
|
|
||||||
* as the asserting party.
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* @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);
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* @return the {@link Saml2LoginConfigurer} for further customizations
|
|
||||||
* @throws Exception
|
|
||||||
* @since 5.5
|
|
||||||
*/
|
|
||||||
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
|
* Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0
|
||||||
* Provider. <br>
|
* Provider. <br>
|
||||||
|
|
|
@ -221,10 +221,6 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||||
// Setup auto-redirect to provider login page
|
// Setup auto-redirect to provider login page
|
||||||
// when only 1 IDP is configured
|
// when only 1 IDP is configured
|
||||||
this.updateAuthenticationDefaults();
|
this.updateAuthenticationDefaults();
|
||||||
Saml2LogoutConfigurer<B> logoutConfigurer = http.getConfigurer(Saml2LogoutConfigurer.class);
|
|
||||||
if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
|
|
||||||
logoutConfigurer.logoutSuccessUrl("/login?logout");
|
|
||||||
}
|
|
||||||
this.updateAccessDefaults(http);
|
this.updateAccessDefaults(http);
|
||||||
String loginUrl = providerUrlMap.entrySet().iterator().next().getKey();
|
String loginUrl = providerUrlMap.entrySet().iterator().next().getKey();
|
||||||
final LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(loginUrl);
|
final LoginUrlAuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(loginUrl);
|
||||||
|
|
|
@ -1,637 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2002-2021 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.security.config.annotation.web.configurers.saml2;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
import org.opensaml.core.Version;
|
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext;
|
|
||||||
import org.springframework.security.config.Customizer;
|
|
||||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
|
||||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.RelyingPartyRegistrationResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutRequestResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml3LogoutResponseResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutRequestResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSaml4LogoutResponseResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutRequestHandler;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.OpenSamlLogoutResponseHandler;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestSuccessHandler;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseSuccessHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.LogoutFilter;
|
|
||||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
|
||||||
import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
|
||||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
|
||||||
import org.springframework.util.Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds SAML 2.0 logout support.
|
|
||||||
*
|
|
||||||
* <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 initiate SAML 2.0 Logout</li>
|
|
||||||
* <li>{@link #logoutRequestMatcher} - The {@link RequestMatcher} to initiate SAML 2.0
|
|
||||||
* Logout</li>
|
|
||||||
* <li>{@link #logoutSuccessHandler} - The {@link LogoutSuccessHandler} to execute once
|
|
||||||
* SAML 2.0 Logout is complete</li>
|
|
||||||
* <li>{@link LogoutRequestConfigurer#logoutRequestMatcher} - The {@link RequestMatcher}
|
|
||||||
* to receive SAML 2.0 Logout Requests</li>
|
|
||||||
* <li>{@link LogoutRequestConfigurer#logoutHandler} - The {@link LogoutHandler} for
|
|
||||||
* processing 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#logoutRequestMatcher} - The {@link RequestMatcher}
|
|
||||||
* to receive SAML 2.0 Logout Responses</li>
|
|
||||||
* <li>{@link LogoutResponseConfigurer#logoutHandler} - The {@link LogoutHandler} for
|
|
||||||
* processing 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.5
|
|
||||||
* @see Saml2LogoutConfigurer
|
|
||||||
*/
|
|
||||||
public final class Saml2LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
|
||||||
extends AbstractHttpConfigurer<Saml2LogoutConfigurer<H>, H> {
|
|
||||||
|
|
||||||
private ApplicationContext context;
|
|
||||||
|
|
||||||
private List<LogoutHandler> logoutHandlers = new ArrayList<>();
|
|
||||||
|
|
||||||
private SecurityContextLogoutHandler contextLogoutHandler = new SecurityContextLogoutHandler();
|
|
||||||
|
|
||||||
private String logoutSuccessUrl = "/login?logout";
|
|
||||||
|
|
||||||
private LogoutSuccessHandler logoutSuccessHandler;
|
|
||||||
|
|
||||||
private String logoutUrl = "/logout";
|
|
||||||
|
|
||||||
private RequestMatcher logoutRequestMatcher;
|
|
||||||
|
|
||||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
|
||||||
|
|
||||||
private LogoutRequestConfigurer logoutRequestConfigurer;
|
|
||||||
|
|
||||||
private LogoutResponseConfigurer logoutResponseConfigurer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new instance
|
|
||||||
* @see HttpSecurity#logout()
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer(ApplicationContext context) {
|
|
||||||
this.context = context;
|
|
||||||
this.logoutRequestConfigurer = new LogoutRequestConfigurer();
|
|
||||||
this.logoutResponseConfigurer = new LogoutResponseConfigurer(this.logoutRequestConfigurer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a {@link LogoutHandler}. {@link SecurityContextLogoutHandler} and
|
|
||||||
* {@link LogoutSuccessEventPublishingLogoutHandler} are added as last
|
|
||||||
* {@link LogoutHandler} instances by default.
|
|
||||||
* @param logoutHandler the {@link LogoutHandler} to add
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> addLogoutHandler(LogoutHandler logoutHandler) {
|
|
||||||
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
|
|
||||||
this.logoutHandlers.add(logoutHandler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies if {@link SecurityContextLogoutHandler} should clear the
|
|
||||||
* {@link Authentication} at the time of logout.
|
|
||||||
* @param clearAuthentication true {@link SecurityContextLogoutHandler} should clear
|
|
||||||
* the {@link Authentication} (default), or false otherwise.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> clearAuthentication(boolean clearAuthentication) {
|
|
||||||
this.contextLogoutHandler.setClearAuthentication(clearAuthentication);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configures {@link SecurityContextLogoutHandler} to invalidate the
|
|
||||||
* {@link HttpSession} at the time of logout.
|
|
||||||
* @param invalidateHttpSession true if the {@link HttpSession} should be invalidated
|
|
||||||
* (default), or false otherwise.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> invalidateHttpSession(boolean invalidateHttpSession) {
|
|
||||||
this.contextLogoutHandler.setInvalidateHttpSession(invalidateHttpSession);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL that triggers log out to occur (default is "/logout"). If CSRF protection
|
|
||||||
* is enabled (default), then the request must also be a POST. This means that by
|
|
||||||
* default POST "/logout" is required to trigger a log out. If CSRF protection is
|
|
||||||
* disabled, then any HTTP method is allowed.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* It is considered best practice to use an HTTP POST on any action that changes state
|
|
||||||
* (i.e. log out) to protect against
|
|
||||||
* <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">CSRF
|
|
||||||
* attacks</a>. If you really want to use an HTTP GET, you can use
|
|
||||||
* <code>logoutRequestMatcher(new AntPathRequestMatcher(logoutUrl, "GET"));</code>
|
|
||||||
* </p>
|
|
||||||
* @param logoutUrl the URL that will invoke logout.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
* @see #logoutRequestMatcher(RequestMatcher)
|
|
||||||
* @see HttpSecurity#csrf()
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> logoutUrl(String logoutUrl) {
|
|
||||||
this.logoutRequestMatcher = null;
|
|
||||||
this.logoutUrl = logoutUrl;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The RequestMatcher that triggers log out to occur. In most circumstances users will
|
|
||||||
* use {@link #logoutUrl(String)} which helps enforce good practices.
|
|
||||||
* @param logoutRequestMatcher the RequestMatcher used to determine if logout should
|
|
||||||
* occur.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
* @see #logoutUrl(String)
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> logoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
|
|
||||||
this.logoutUrl = null;
|
|
||||||
this.logoutRequestMatcher = logoutRequestMatcher;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The URL to redirect to after logout has occurred. The default is "/login?logout".
|
|
||||||
* This is a shortcut for invoking {@link #logoutSuccessHandler(LogoutSuccessHandler)}
|
|
||||||
* with a {@link SimpleUrlLogoutSuccessHandler}.
|
|
||||||
* @param logoutSuccessUrl the URL to redirect to after logout occurred
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> logoutSuccessUrl(String logoutSuccessUrl) {
|
|
||||||
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
|
|
||||||
logoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl);
|
|
||||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the {@link LogoutSuccessHandler} to use. If this is specified,
|
|
||||||
* {@link #logoutSuccessUrl(String)} is ignored.
|
|
||||||
* @param logoutSuccessHandler the {@link LogoutSuccessHandler} to use after a user
|
|
||||||
* has been logged out.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
|
|
||||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows specifying the names of cookies to be removed on logout success. This is a
|
|
||||||
* shortcut to easily invoke {@link #addLogoutHandler(LogoutHandler)} with a
|
|
||||||
* {@link CookieClearingLogoutHandler}.
|
|
||||||
* @param cookieNamesToClear the names of cookies to be removed on logout success.
|
|
||||||
* @return the {@link Saml2LogoutConfigurer} for further customization
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<H> deleteCookies(String... cookieNamesToClear) {
|
|
||||||
return addLogoutHandler(new CookieClearingLogoutHandler(cookieNamesToClear));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the {@code RelyingPartyRegistrationRepository} of relying parties, each party
|
|
||||||
* representing a service provider, SP and this host, and identity provider, IDP pair
|
|
||||||
* that communicate with each other.
|
|
||||||
* @param repo the repository of relying parties
|
|
||||||
* @return the {@link Saml2LoginConfigurer} for further configuration
|
|
||||||
*/
|
|
||||||
public Saml2LogoutConfigurer<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 {
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver = getRelyingPartyRegistrationResolver(http);
|
|
||||||
CsrfTokenRepository csrfTokenRepository = http.getSharedObject(CsrfTokenRepository.class);
|
|
||||||
if (csrfTokenRepository != null) {
|
|
||||||
this.logoutHandlers.add(new CsrfLogoutHandler(csrfTokenRepository));
|
|
||||||
}
|
|
||||||
this.logoutHandlers.add(this.contextLogoutHandler);
|
|
||||||
this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler()));
|
|
||||||
LogoutFilter logoutFilter = createLogoutFilter(http, this.logoutHandlers, relyingPartyRegistrationResolver);
|
|
||||||
http.addFilterBefore(logoutFilter, LogoutFilter.class);
|
|
||||||
Saml2LogoutRequestFilter logoutRequestFilter = createLogoutRequestFilter(this.logoutHandlers,
|
|
||||||
relyingPartyRegistrationResolver);
|
|
||||||
http.addFilterBefore(logoutRequestFilter, LogoutFilter.class);
|
|
||||||
Saml2LogoutResponseFilter logoutResponseFilter = createLogoutResponseFilter(relyingPartyRegistrationResolver);
|
|
||||||
logoutResponseFilter.setLogoutSuccessHandler(getLogoutSuccessHandler());
|
|
||||||
http.addFilterBefore(logoutResponseFilter, LogoutFilter.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the logout success has been customized via
|
|
||||||
* {@link #logoutSuccessUrl(String)} or
|
|
||||||
* {@link #logoutSuccessHandler(LogoutSuccessHandler)}.
|
|
||||||
* @return true if logout success handling has been customized, else false
|
|
||||||
*/
|
|
||||||
boolean isCustomLogoutSuccess() {
|
|
||||||
return this.logoutSuccessHandler != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RelyingPartyRegistrationResolver getRelyingPartyRegistrationResolver(H http) {
|
|
||||||
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository();
|
|
||||||
return new DefaultRelyingPartyRegistrationResolver(registrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository() {
|
|
||||||
if (this.relyingPartyRegistrationRepository == null) {
|
|
||||||
this.relyingPartyRegistrationRepository = getBeanOrNull(RelyingPartyRegistrationRepository.class);
|
|
||||||
}
|
|
||||||
return this.relyingPartyRegistrationRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutFilter createLogoutFilter(H http, List<LogoutHandler> logoutHandlers,
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
LogoutHandler[] handlers = logoutHandlers.toArray(new LogoutHandler[0]);
|
|
||||||
LogoutSuccessHandler logoutRequestSuccessHandler = this.logoutRequestConfigurer
|
|
||||||
.logoutRequestSuccessHandler(relyingPartyRegistrationResolver);
|
|
||||||
LogoutSuccessHandler finalSuccessHandler = getLogoutSuccessHandler();
|
|
||||||
LogoutSuccessHandler logoutSuccessHandler = (request, response, authentication) -> {
|
|
||||||
if (authentication == null) {
|
|
||||||
finalSuccessHandler.onLogoutSuccess(request, response, authentication);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
logoutRequestSuccessHandler.onLogoutSuccess(request, response, authentication);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
LogoutFilter result = new LogoutFilter(logoutSuccessHandler, handlers) {
|
|
||||||
@Override
|
|
||||||
protected boolean requiresLogout(HttpServletRequest request, HttpServletResponse response) {
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
|
||||||
if (!(authentication instanceof Saml2Authentication)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return super.requiresLogout(request, response);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
|
|
||||||
return postProcess(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2LogoutRequestFilter createLogoutRequestFilter(List<LogoutHandler> logoutHandlers,
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
LogoutHandler logoutRequestHandler = this.logoutRequestConfigurer
|
|
||||||
.logoutRequestHandler(relyingPartyRegistrationResolver);
|
|
||||||
List<LogoutHandler> handlers = new ArrayList<>();
|
|
||||||
handlers.add(logoutRequestHandler);
|
|
||||||
handlers.addAll(logoutHandlers);
|
|
||||||
Saml2LogoutRequestFilter logoutRequestFilter = new Saml2LogoutRequestFilter(
|
|
||||||
this.logoutResponseConfigurer.logoutResponseSuccessHandler(relyingPartyRegistrationResolver),
|
|
||||||
new CompositeLogoutHandler(handlers));
|
|
||||||
logoutRequestFilter.setLogoutRequestMatcher(this.logoutRequestConfigurer.requestMatcher);
|
|
||||||
CsrfConfigurer<H> csrf = getBuilder().getConfigurer(CsrfConfigurer.class);
|
|
||||||
if (csrf != null) {
|
|
||||||
csrf.ignoringRequestMatchers(this.logoutRequestConfigurer.requestMatcher);
|
|
||||||
}
|
|
||||||
return logoutRequestFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2LogoutResponseFilter createLogoutResponseFilter(
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(
|
|
||||||
this.logoutResponseConfigurer.logoutResponseHandler(relyingPartyRegistrationResolver));
|
|
||||||
logoutResponseFilter.setLogoutRequestMatcher(this.logoutResponseConfigurer.requestMatcher);
|
|
||||||
CsrfConfigurer<H> csrf = getBuilder().getConfigurer(CsrfConfigurer.class);
|
|
||||||
if (csrf != null) {
|
|
||||||
csrf.ignoringRequestMatchers(this.logoutResponseConfigurer.requestMatcher);
|
|
||||||
}
|
|
||||||
logoutResponseFilter.setLogoutSuccessHandler(getLogoutSuccessHandler());
|
|
||||||
return logoutResponseFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestMatcher getLogoutRequestMatcher(H http) {
|
|
||||||
if (this.logoutRequestMatcher != null) {
|
|
||||||
return this.logoutRequestMatcher;
|
|
||||||
}
|
|
||||||
this.logoutRequestMatcher = createLogoutRequestMatcher(http);
|
|
||||||
return this.logoutRequestMatcher;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private RequestMatcher createLogoutRequestMatcher(H http) {
|
|
||||||
RequestMatcher post = createLogoutRequestMatcher("POST");
|
|
||||||
if (http.getConfigurer(CsrfConfigurer.class) != null) {
|
|
||||||
return post;
|
|
||||||
}
|
|
||||||
RequestMatcher get = createLogoutRequestMatcher("GET");
|
|
||||||
return new OrRequestMatcher(get, post);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RequestMatcher createLogoutRequestMatcher(String httpMethod) {
|
|
||||||
return new AntPathRequestMatcher(this.logoutUrl, httpMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutSuccessHandler getLogoutSuccessHandler() {
|
|
||||||
if (this.logoutSuccessHandler != null) {
|
|
||||||
return this.logoutSuccessHandler;
|
|
||||||
}
|
|
||||||
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
|
|
||||||
logoutSuccessHandler.setDefaultTargetUrl(this.logoutSuccessUrl);
|
|
||||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
|
||||||
return logoutSuccessHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private <C> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A configurer for SAML 2.0 LogoutRequest components
|
|
||||||
*/
|
|
||||||
public final class LogoutRequestConfigurer {
|
|
||||||
|
|
||||||
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
|
|
||||||
|
|
||||||
private LogoutHandler logoutHandler;
|
|
||||||
|
|
||||||
private LogoutSuccessHandler logoutSuccessHandler;
|
|
||||||
|
|
||||||
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
|
|
||||||
|
|
||||||
LogoutRequestConfigurer() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link RequestMatcher} for recognizing a logout request from the
|
|
||||||
* asserting party
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Defaults to {@code /logout/saml2}
|
|
||||||
* @param requestMatcher the {@link RequestMatcher} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutRequestConfigurer logoutRequestMatcher(RequestMatcher requestMatcher) {
|
|
||||||
this.requestMatcher = requestMatcher;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link LogoutHandler} for processing a logout request from the
|
|
||||||
* asserting party
|
|
||||||
* @param logoutHandler the {@link LogoutHandler} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutRequestConfigurer logoutRequestHandler(LogoutHandler logoutHandler) {
|
|
||||||
this.logoutHandler = logoutHandler;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link Saml2LogoutRequestResolver} for producing a logout request to
|
|
||||||
* send to the asserting party
|
|
||||||
* @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutRequestConfigurer logoutRequestResolver(Saml2LogoutRequestResolver logoutRequestResolver) {
|
|
||||||
this.logoutSuccessHandler = new Saml2LogoutRequestSuccessHandler(logoutRequestResolver);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link Saml2LogoutRequestRepository} for storing logout requests
|
|
||||||
* @param logoutRequestRepository the {@link Saml2LogoutRequestRepository} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutRequestConfigurer logoutRequestRepository(Saml2LogoutRequestRepository logoutRequestRepository) {
|
|
||||||
this.logoutRequestRepository = logoutRequestRepository;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Saml2LogoutConfigurer<H> and() {
|
|
||||||
return Saml2LogoutConfigurer.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutHandler logoutRequestHandler(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (this.logoutHandler == null) {
|
|
||||||
return new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
return this.logoutHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutSuccessHandler logoutRequestSuccessHandler(
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (this.logoutSuccessHandler == null) {
|
|
||||||
Saml2LogoutRequestSuccessHandler logoutSuccessHandler = new Saml2LogoutRequestSuccessHandler(
|
|
||||||
logoutRequestResolver(relyingPartyRegistrationResolver));
|
|
||||||
logoutSuccessHandler.setLogoutRequestRepository(this.logoutRequestRepository);
|
|
||||||
return logoutSuccessHandler;
|
|
||||||
}
|
|
||||||
return this.logoutSuccessHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2LogoutRequestResolver logoutRequestResolver(
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (Version.getVersion().startsWith("4")) {
|
|
||||||
return new OpenSaml4LogoutRequestResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
return new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class LogoutResponseConfigurer {
|
|
||||||
|
|
||||||
private final LogoutRequestConfigurer logoutRequest;
|
|
||||||
|
|
||||||
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/logout/saml2/slo");
|
|
||||||
|
|
||||||
private LogoutHandler logoutHandler;
|
|
||||||
|
|
||||||
private LogoutSuccessHandler logoutSuccessHandler;
|
|
||||||
|
|
||||||
LogoutResponseConfigurer(LogoutRequestConfigurer logoutRequest) {
|
|
||||||
this.logoutRequest = logoutRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link RequestMatcher} for recognizing a logout response from the
|
|
||||||
* asserting party
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* Defaults to {@code /logout/saml2}
|
|
||||||
* @param requestMatcher the {@link RequestMatcher} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutResponseConfigurer logoutRequestMatcher(RequestMatcher requestMatcher) {
|
|
||||||
this.requestMatcher = requestMatcher;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link LogoutHandler} for processing a logout response from the
|
|
||||||
* asserting party
|
|
||||||
* @param logoutHandler the {@link LogoutHandler} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutResponseConfigurer logoutResponseHandler(LogoutHandler logoutHandler) {
|
|
||||||
this.logoutHandler = logoutHandler;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this {@link Saml2LogoutRequestResolver} for producing a logout response to
|
|
||||||
* send to the asserting party
|
|
||||||
* @param logoutResponseResolver the {@link Saml2LogoutResponseResolver} to use
|
|
||||||
* @return the {@link LogoutRequestConfigurer} for further customizations
|
|
||||||
*/
|
|
||||||
public LogoutResponseConfigurer logoutResponseResolver(Saml2LogoutResponseResolver logoutResponseResolver) {
|
|
||||||
this.logoutSuccessHandler = new Saml2LogoutResponseSuccessHandler(logoutResponseResolver);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Saml2LogoutConfigurer<H> and() {
|
|
||||||
return Saml2LogoutConfigurer.this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutHandler logoutResponseHandler(RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (this.logoutHandler == null) {
|
|
||||||
OpenSamlLogoutResponseHandler logoutHandler = new OpenSamlLogoutResponseHandler(
|
|
||||||
relyingPartyRegistrationResolver);
|
|
||||||
logoutHandler.setLogoutRequestRepository(this.logoutRequest.logoutRequestRepository);
|
|
||||||
return logoutHandler;
|
|
||||||
}
|
|
||||||
return this.logoutHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LogoutSuccessHandler logoutResponseSuccessHandler(
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (this.logoutSuccessHandler == null) {
|
|
||||||
return new Saml2LogoutResponseSuccessHandler(logoutResponseResolver(relyingPartyRegistrationResolver));
|
|
||||||
}
|
|
||||||
return this.logoutSuccessHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2LogoutResponseResolver logoutResponseResolver(
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver) {
|
|
||||||
if (Version.getVersion().startsWith("4")) {
|
|
||||||
return new OpenSaml4LogoutResponseResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
return new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,400 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2002-2021 the original author or authors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.springframework.security.config.annotation.web.configurers.saml2;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.opensaml.saml.saml2.core.LogoutRequest;
|
|
||||||
import org.opensaml.xmlsec.signature.support.SignatureConstants;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.mock.web.MockFilterChain;
|
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
|
||||||
import org.springframework.mock.web.MockHttpServletResponse;
|
|
||||||
import org.springframework.mock.web.MockHttpSession;
|
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
|
||||||
import org.springframework.security.config.test.SpringTestRule;
|
|
||||||
import org.springframework.security.core.authority.AuthorityUtils;
|
|
||||||
import org.springframework.security.saml2.Saml2Exception;
|
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
|
||||||
import org.springframework.security.saml2.core.TestSaml2X509Credentials;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.TestOpenSamlObjects;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutResponse;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.HttpSessionLogoutRequestRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver.Saml2LogoutResponseBuilder;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
|
||||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import org.springframework.test.web.servlet.MvcResult;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.BDDMockito.RETURNS_SELF;
|
|
||||||
import static org.mockito.BDDMockito.given;
|
|
||||||
import static org.mockito.BDDMockito.mock;
|
|
||||||
import static org.mockito.BDDMockito.reset;
|
|
||||||
import static org.mockito.BDDMockito.verify;
|
|
||||||
import static org.mockito.BDDMockito.verifyNoInteractions;
|
|
||||||
import static org.mockito.BDDMockito.willAnswer;
|
|
||||||
import static org.mockito.BDDMockito.willReturn;
|
|
||||||
import static org.springframework.security.config.Customizer.withDefaults;
|
|
||||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
|
||||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests for different Java configuration for {@link Saml2LogoutConfigurer}
|
|
||||||
*/
|
|
||||||
public class Saml2LogoutConfigurerTests {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ConfigurableApplicationContext context;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RelyingPartyRegistrationRepository repository;
|
|
||||||
|
|
||||||
private final Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
public final SpringTestRule spring = new SpringTestRule();
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
MockMvc mvc;
|
|
||||||
|
|
||||||
private Saml2Authentication user = new Saml2Authentication(
|
|
||||||
new DefaultSaml2AuthenticatedPrincipal("user", Collections.emptyMap()), "response",
|
|
||||||
AuthorityUtils.createAuthorityList("ROLE_USER"), "registration-id");
|
|
||||||
|
|
||||||
String apLogoutRequest = "nZFBa4MwGIb/iuQeE2NTXFDLQAaC26Hrdtgt1dQFNMnyxdH9+zlboeyww275SN7nzcOX787jEH0qD9qaAiUxRZEyre206Qv0cnjAGdqVOchxYE40trdT2KuPSUGI5qQBcbkq0OSNsBI0CCNHBSK04vn+sREspsJ5G2xrBxRVc1AbGZa29xAcCEK8i9VZjm5QsfU9GZYWsoCJv5ShqK4K1Ow5p5LyU4aP6XaLN3cpw9mGctydjrxNaZt1XM5vASZVGwjShAIxyhJMU8z4gSWCM8GSmDH+hqLX1Xv+JLpaiiXsb+3+lpMAyv8IoVI6rEzQ4QvrLie3uBX+NMfr6l/waT6t0AumvI6/FlN+Aw==";
|
|
||||||
|
|
||||||
String apLogoutRequestSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
|
|
||||||
|
|
||||||
String apLogoutRequestRelayState = "33591874-b123-4f2c-ab0d-2d0d84aa8b56";
|
|
||||||
|
|
||||||
String apLogoutRequestSignature = "oKqdzrmn2YAqXcwkow2lzRXr5PNHm0s/gWsRnaZYhC+Oq5ekK5uIKQYvtmNR94HJjDe1VRs+vVQCYivgdoTzBV2ZlffTXZmYsCsY9q4jbCWR6R5CbhU73/MkKQsPcyVvMhNYxnDYapIlxDsfoZNTboDEz3GM+HRoGRfl9emCXY0lPRYwqC4kpu7oMDBkafR0A09jPIxFuNpqlLPwUxL9m+DGkvDK3mFDN1xJcgZaK73HcuJe7Qh4huOrKNFetwc5EvqfiwgiWF6sfq9A+rZBfCIYo10NNLY7fNQAR2IqwcKtawHgTGWbeshRyFrwVYMR64EnClfxUHsHKf5kiZ2dlw==";
|
|
||||||
|
|
||||||
String apLogoutResponse = "fZHRa4MwEMb/Fcl7jEadGqplrAwK3Uvb9WFvZ4ydoInk4uj++1nXbmWMvhwcd9/3Jb9bLE99530oi63RBQn9gHhKS1O3+liQ1/0zzciyXCD0HR/ExhzN6LYKB6NReZNUo/ieFWS0WhjAFoWGXqFwUuweXzaC+4EYrHFGmo54K4Wu1eDmuHfnBhSM2cFXJ+iHTvnGHlk3x7DZmNlLGvHWq4Jstk0GUSjjiIZJI2lcpQnNeRLTAOo4fwCeQg3Trr6+cm/OqmnWVHECVGWQ0jgCSatsKvXUxhFvZF7xSYU4qrVGB9oVhAc8pEFEebLnkeBc8NyPePpGvMOV1/Q3cqEjZrG9hXKfCSAqe+ZAShio0q51n7StF+zW7gf9zoEb8U/7ZGrlHaAb1f0onLfFbpRSIRJWXkJ+bdm/Fy6/AA==";
|
|
||||||
|
|
||||||
String apLogoutResponseSigAlg = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
|
|
||||||
|
|
||||||
String apLogoutResponseRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315";
|
|
||||||
|
|
||||||
String apLogoutResponseSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q==";
|
|
||||||
|
|
||||||
String rpLogoutRequest = "nZFBa4MwGIb/iuQeY6NlGtQykIHgdui6HXaLmrqAJlm+OLp/v0wrlB122CXkI3mfNw/JD5dpDD6FBalVgXZhhAKhOt1LNRTo5fSAU3Qoc+DTSA1r9KBndxQfswAX+KQCth4VaLaKaQ4SmOKTAOY69nz/2DAaRsxY7XSnRxRUPigVd0vbu3MGGCHchOLCJzOKUNuBjEsLWcDErmUoqKsCNcc+yc5tsudYpPwOJzHvcJv6pfdjEtNzl7XU3wWYRa3AceUKRCO6w1GM6f5EY0Ypo1lIk+gNBa+bt38kulqyJWxv7f6W4wDC/gih0hoslJPuC8s+J7e4Df7k43X1L/jsdxt0xZTX8dfHlN8=";
|
|
||||||
|
|
||||||
String rpLogoutRequestId = "LRd49fb45a-e8a7-43ac-b8ac-d8a7432fc9b2";
|
|
||||||
|
|
||||||
String rpLogoutRequestRelayState = "8f63887a-ec7e-4149-b6a0-dd730017f315";
|
|
||||||
|
|
||||||
String rpLogoutRequestSignature = "h2fDqSIBfmnkRHKDMY4IxkCXcI0w98ydNsnPmv1b7GTZCWLbJ+oxaP2yZNPw7wOWXTv86cTPwKLjx5halKy5C+hhWnT0haKhuMcUvHlsgAMBbJKLV+1afzL4O77cvAQJmMNRK7ugXGNV5PTEnd1U4voy134OgdD5XycYiFVRZOwP5H84eJ9xxlvqQwqDvZTcgiF/ZS4ioZgzgnIFcbagZQ12LWNh26OMaUpIW04kCeO6t2dUsxOL6nZWvNrX/Zx1sORIpu4doDUa1RYC8YnjZeQEzDqUVC/dBO/mbVJ/hbF9tD0jBUx7YIgoXpqsWK4TcCsvmlmhrJXvGxDyoAWu2Q==";
|
|
||||||
|
|
||||||
private MockHttpServletRequest request;
|
|
||||||
|
|
||||||
private MockHttpServletResponse response;
|
|
||||||
|
|
||||||
private MockFilterChain filterChain;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setup() {
|
|
||||||
this.request = new MockHttpServletRequest("POST", "");
|
|
||||||
this.request.setServletPath("/login/saml2/sso/test-rp");
|
|
||||||
this.response = new MockHttpServletResponse();
|
|
||||||
this.filterChain = new MockFilterChain();
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
public void cleanup() {
|
|
||||||
if (this.context != null) {
|
|
||||||
this.context.close();
|
|
||||||
}
|
|
||||||
reset(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenDefaultsThenLogsOutAndSendsLogoutRequest() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
MvcResult result = this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()))
|
|
||||||
.andExpect(status().isFound()).andReturn();
|
|
||||||
String location = result.getResponse().getHeader("Location");
|
|
||||||
assertThat(location).startsWith("https://ap.example.org/logout/saml2/request");
|
|
||||||
verify(Saml2LogoutDefaultsConfig.mockLogoutHandler).logout(any(), any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenUnauthenticatedThenEntryPoint() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isFound())
|
|
||||||
.andExpect(redirectedUrl("/login?logout"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenMissingCsrfThen403() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
this.mvc.perform(post("/logout").with(authentication(this.user))).andExpect(status().isForbidden());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenGetThenDefaultLogoutPage() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
MvcResult result = this.mvc.perform(get("/logout").with(authentication(this.user)).with(csrf()))
|
|
||||||
.andExpect(status().isOk()).andReturn();
|
|
||||||
assertThat(result.getResponse().getContentAsString()).contains("Are you sure you want to log out?");
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenPutOrDeleteThen404() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
this.mvc.perform(put("/logout").with(authentication(this.user)).with(csrf())).andExpect(status().isNotFound());
|
|
||||||
this.mvc.perform(delete("/logout").with(authentication(this.user)).with(csrf()))
|
|
||||||
.andExpect(status().isNotFound());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenNoRegistrationThenIllegalArgument() {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
Saml2Authentication authentication = new Saml2Authentication(
|
|
||||||
new DefaultSaml2AuthenticatedPrincipal("user", Collections.emptyMap()), "response",
|
|
||||||
AuthorityUtils.createAuthorityList("ROLE_USER"), "wrong");
|
|
||||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(
|
|
||||||
() -> this.mvc.perform(post("/logout").with(authentication(authentication)).with(csrf())).andReturn());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutWhenCustomLogoutRequestResolverThenUses() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
|
|
||||||
this.mvc.perform(post("/logout").with(authentication(this.user)).with(csrf()));
|
|
||||||
verify(Saml2LogoutComponentsConfig.logoutRequestResolver).resolveLogoutRequest(any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutRequestWhenDefaultsThenLogsOutAndSendsLogoutResponse() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
MvcResult result = this.mvc
|
|
||||||
.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
|
|
||||||
.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
|
|
||||||
.param("Signature", this.apLogoutRequestSignature).with(authentication(this.user)))
|
|
||||||
.andExpect(status().isFound()).andReturn();
|
|
||||||
String location = result.getResponse().getHeader("Location");
|
|
||||||
assertThat(location).startsWith("https://ap.example.org/logout/saml2/response");
|
|
||||||
verify(Saml2LogoutDefaultsConfig.mockLogoutHandler).logout(any(), any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutRequestWhenNoRegistrationThenIllegalArgument() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
assertThatExceptionOfType(IllegalArgumentException.class)
|
|
||||||
.isThrownBy(() -> this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
|
|
||||||
.param("RelayState", this.apLogoutRequestRelayState).param("SigAlg", this.apLogoutRequestSigAlg)
|
|
||||||
.param("Signature", this.apLogoutRequestSignature)).andReturn());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutRequestWhenNoSamlRequestThen404() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
this.mvc.perform(get("/logout/saml2/slo").with(authentication(this.user))).andExpect(status().isNotFound());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutRequestWhenInvalidSamlRequestThenException() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
assertThatExceptionOfType(Saml2Exception.class)
|
|
||||||
.isThrownBy(() -> this.mvc
|
|
||||||
.perform(get("/logout/saml2/slo").param("SAMLRequest", this.apLogoutRequest)
|
|
||||||
.param("RelayState", this.apLogoutRequestRelayState)
|
|
||||||
.param("SigAlg", this.apLogoutRequestSigAlg).with(authentication(this.user)))
|
|
||||||
.andReturn());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutRequestWhenCustomLogoutRequestHandlerThenUses() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
|
|
||||||
RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
|
|
||||||
LogoutRequest logoutRequest = TestOpenSamlObjects.assertingPartyLogoutRequest(registration);
|
|
||||||
logoutRequest.setIssueInstant(Instant.now());
|
|
||||||
willAnswer((invocation) -> {
|
|
||||||
HttpServletRequest request = (HttpServletRequest) invocation.getArguments()[0];
|
|
||||||
request.setAttribute(LogoutRequest.class.getName(), logoutRequest);
|
|
||||||
return null;
|
|
||||||
}).given(Saml2LogoutComponentsConfig.logoutRequestHandler).logout(any(), any(), any());
|
|
||||||
Saml2LogoutResponseBuilder<?> partial = mock(Saml2LogoutResponseBuilder.class, RETURNS_SELF);
|
|
||||||
given(partial.logoutResponse())
|
|
||||||
.willReturn(Saml2LogoutResponse.withRelyingPartyRegistration(registration).build());
|
|
||||||
willReturn(partial).given(Saml2LogoutComponentsConfig.logoutResponseResolver).resolveLogoutResponse(any(),
|
|
||||||
any());
|
|
||||||
this.mvc.perform(get("/logout/saml2/slo").param("SAMLRequest", "samlRequest")).andReturn();
|
|
||||||
verify(Saml2LogoutComponentsConfig.logoutRequestHandler).logout(any(), any(), any());
|
|
||||||
verify(Saml2LogoutComponentsConfig.logoutResponseResolver).resolveLogoutResponse(any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutResponseWhenDefaultsThenRedirectsAndDoesNotLogout() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
|
|
||||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
|
||||||
.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
|
|
||||||
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
|
|
||||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response);
|
|
||||||
this.request.setParameter("RelayState", logoutRequest.getRelayState());
|
|
||||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNotNull();
|
|
||||||
this.mvc.perform(get("/logout/saml2/slo").session(((MockHttpSession) this.request.getSession()))
|
|
||||||
.param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState)
|
|
||||||
.param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature))
|
|
||||||
.andExpect(status().isFound()).andExpect(redirectedUrl("/login?logout"));
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
assertThat(this.logoutRequestRepository.loadLogoutRequest(this.request)).isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutResponseWhenNoMatchingLogoutRequestThenSaml2Exception() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(() -> this.mvc.perform(get("/logout/saml2/slo")
|
|
||||||
.param("SAMLResponse", this.apLogoutResponse).param("RelayState", this.apLogoutResponseRelayState)
|
|
||||||
.param("SigAlg", this.apLogoutResponseSigAlg).param("Signature", this.apLogoutResponseSignature)));
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutResponseWhenNoSamlResponseThenEntryPoint() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
this.mvc.perform(get("/logout/saml2/slo")).andExpect(status().isFound())
|
|
||||||
.andExpect(redirectedUrl("http://localhost/login"));
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutResponseWhenInvalidSamlResponseThenException() {
|
|
||||||
this.spring.register(Saml2LogoutDefaultsConfig.class).autowire();
|
|
||||||
RelyingPartyRegistration registration = this.repository.findByRegistrationId("registration-id");
|
|
||||||
Saml2LogoutRequest logoutRequest = Saml2LogoutRequest.withRelyingPartyRegistration(registration)
|
|
||||||
.samlRequest(this.rpLogoutRequest).id(this.rpLogoutRequestId).relayState(this.rpLogoutRequestRelayState)
|
|
||||||
.parameters((params) -> params.put("Signature", this.rpLogoutRequestSignature)).build();
|
|
||||||
this.logoutRequestRepository.saveLogoutRequest(logoutRequest, this.request, this.response);
|
|
||||||
assertThatExceptionOfType(Saml2Exception.class).isThrownBy(
|
|
||||||
() -> this.mvc.perform(get("/logout/saml2/slo").session((MockHttpSession) this.request.getSession())
|
|
||||||
.param("SAMLResponse", this.apLogoutRequest).param("RelayState", this.apLogoutRequestRelayState)
|
|
||||||
.param("SigAlg", this.apLogoutRequestSigAlg)).andReturn());
|
|
||||||
verifyNoInteractions(Saml2LogoutDefaultsConfig.mockLogoutHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void saml2LogoutResponseWhenCustomLogoutResponseHandlerThenUses() throws Exception {
|
|
||||||
this.spring.register(Saml2LogoutComponentsConfig.class).autowire();
|
|
||||||
this.mvc.perform(get("/logout/saml2/slo").param("SAMLResponse", "samlResponse")).andReturn();
|
|
||||||
verify(Saml2LogoutComponentsConfig.logoutResponseHandler).logout(any(), any(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnableWebSecurity
|
|
||||||
@Import(Saml2LoginConfigBeans.class)
|
|
||||||
static class Saml2LogoutDefaultsConfig {
|
|
||||||
|
|
||||||
static final LogoutHandler mockLogoutHandler = mock(LogoutHandler.class);
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
|
||||||
http.authorizeRequests((authorize) -> authorize.anyRequest().authenticated()).saml2Login(withDefaults())
|
|
||||||
.saml2Logout((logout) -> logout.addLogoutHandler(mockLogoutHandler));
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnableWebSecurity
|
|
||||||
@Import(Saml2LoginConfigBeans.class)
|
|
||||||
static class Saml2LogoutComponentsConfig {
|
|
||||||
|
|
||||||
static final Saml2LogoutRequestRepository logoutRequestRepository = mock(Saml2LogoutRequestRepository.class);
|
|
||||||
static final LogoutHandler logoutRequestHandler = mock(LogoutHandler.class);
|
|
||||||
static final Saml2LogoutRequestResolver logoutRequestResolver = mock(Saml2LogoutRequestResolver.class);
|
|
||||||
static final LogoutHandler logoutResponseHandler = mock(LogoutHandler.class);
|
|
||||||
static final Saml2LogoutResponseResolver logoutResponseResolver = mock(Saml2LogoutResponseResolver.class);
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
SecurityFilterChain web(HttpSecurity http) throws Exception {
|
|
||||||
http.authorizeRequests((authorize) -> authorize.anyRequest().authenticated()).saml2Login(withDefaults())
|
|
||||||
.saml2Logout((logout) -> logout
|
|
||||||
.logoutRequest((request) -> request.logoutRequestRepository(logoutRequestRepository)
|
|
||||||
.logoutRequestHandler(logoutRequestHandler)
|
|
||||||
.logoutRequestResolver(logoutRequestResolver))
|
|
||||||
.logoutResponse((response) -> response.logoutResponseHandler(logoutResponseHandler)
|
|
||||||
.logoutResponseResolver(logoutResponseResolver)));
|
|
||||||
return http.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Saml2LoginConfigBeans {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
|
|
||||||
Saml2X509Credential signing = TestSaml2X509Credentials.assertingPartySigningCredential();
|
|
||||||
Saml2X509Credential verification = TestSaml2X509Credentials.relyingPartyVerifyingCredential();
|
|
||||||
RelyingPartyRegistration.Builder withCreds = TestRelyingPartyRegistrations.noCredentials()
|
|
||||||
.signingX509Credentials(credential(signing))
|
|
||||||
.assertingPartyDetails((party) -> party.verificationX509Credentials(credential(verification)));
|
|
||||||
RelyingPartyRegistration registration = withCreds.build();
|
|
||||||
RelyingPartyRegistration ap = withCreds.registrationId("ap").entityId("ap-entity-id")
|
|
||||||
.assertingPartyDetails((party) -> party
|
|
||||||
.singleLogoutServiceLocation("https://rp.example.org/logout/saml2/request")
|
|
||||||
.singleLogoutServiceResponseLocation("https://rp.example.org/logout/saml2/response"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(ap, registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Consumer<Collection<Saml2X509Credential>> credential(Saml2X509Credential credential) {
|
|
||||||
return (credentials) -> credentials.add(credential);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1074,7 +1074,9 @@ 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
|
* 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
|
* Third, your application must have a PKCS#8 private key and X.509 certificate for signing `saml2:LogoutRequest` s and `saml2:LogoutResponse` s
|
||||||
|
|
||||||
You can begin from the initial minimal example and add the following configuration:
|
==== 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:
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
|
@ -1103,15 +1105,28 @@ SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository re
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.saml2Login(withDefaults())
|
.saml2Login(withDefaults())
|
||||||
.saml2Logout(withDefaults()); <2>
|
.logout((logout) -> logout
|
||||||
|
.logoutUrl("/saml2/logout")
|
||||||
|
.logoutSuccessHandler(successHandler))
|
||||||
|
.addFilterBefore(new Saml2LogoutResponseFilter(logoutHandler), CsrfFilter.class);
|
||||||
|
|
||||||
return http.build();
|
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>>
|
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
|
||||||
<2> - Second, indicate that your application wants to use SAML SLO to logout the end user
|
<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.
|
||||||
|
|
||||||
==== Runtime Expectations
|
==== Runtime Expectations for RP-Initiated
|
||||||
|
|
||||||
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
|
Given the above configuration any logged in user can send a `POST /logout` to your application to perform RP-initiated SLO.
|
||||||
Your application will then do the following:
|
Your application will then do the following:
|
||||||
|
@ -1122,6 +1137,63 @@ Your application will then do the following:
|
||||||
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
|
4. Deserialize, verify, and process the `<saml2:LogoutResponse>` sent by the asserting party
|
||||||
5. Redirect to any configured successful logout endpoint
|
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-initated 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
|
1. Use a `Saml2LogoutRequestHandler` to deserialize, verify, and process the `<saml2:LogoutRequest>` sent by the asserting party
|
||||||
|
@ -1129,6 +1201,12 @@ Also, your application can participate in an AP-initated logout when the asserti
|
||||||
3. Create, sign, and serialize a `<saml2:LogoutResponse>` based on the <<servlet-saml2login-relyingpartyregistration,`RelyingPartyRegistration`>> associated with the just logged-out user
|
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`>>
|
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
|
=== Configuring Logout Endpoints
|
||||||
|
|
||||||
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
|
There are three default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
|
||||||
|
@ -1145,12 +1223,11 @@ To reduce changes in configuration for the asserting party, you can configure th
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
RequestMatcher slo = new AntPathRequestMatcher("/SLOService.saml2", "GET");
|
Saml2LogoutResponseFilter filter = new Saml2LogoutResponseFilter(logoutHandler);
|
||||||
|
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
|
||||||
http
|
http
|
||||||
.saml2Logout((saml2) -> saml2
|
// ...
|
||||||
.logoutRequest((request) -> request.logoutRequestMatcher(slo))
|
.addFilterBefore(filter, CsrfFilter.class);
|
||||||
.logoutResponse((response) -> response.logoutRequestMatcher(slo))
|
|
||||||
);
|
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Customizing `<saml2:LogoutRequest>` Resolution
|
=== Customizing `<saml2:LogoutRequest>` Resolution
|
||||||
|
@ -1168,40 +1245,22 @@ To add other values, you can use delegation, like so:
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@Component
|
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
|
||||||
public class MyOpenSamlLogoutRequestResolver implements Saml2LogoutRequestResolver {
|
return (request, response, authentication) -> {
|
||||||
private final OpenSaml3LogoutRequestResolver logoutRequestResolver;
|
OpenSamlLogoutRequestBuilder builder = delegate.resolveLogoutRequest(request, response, authentication); <1>
|
||||||
|
builder.name(((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute")); <2>
|
||||||
public MyOpenSamlLogoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
|
builder.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
|
return builder.logoutRequest(); <3>
|
||||||
new DefaultRelyingPartyRegistrationResolver(registrations);
|
};
|
||||||
this.logoutRequestResolver = new OpenSaml3LogoutRequestResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OpenSamlLogoutRequestBuilder resolveLogoutRequest(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
|
||||||
String name = ((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute");
|
|
||||||
String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
|
|
||||||
return logoutRequestResolver.resolveLogoutRequest(request, authentication) <1>
|
|
||||||
.name(name) <2>
|
|
||||||
.logoutRequest((logoutRequest) -> logoutRequest.getNameID().setFormat(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
----
|
----
|
||||||
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
|
<1> - Spring Security applies default values to a `<saml2:LogoutRequest>`
|
||||||
<2> - Your application specifies customizations
|
<2> - Your application specifies customizations
|
||||||
|
<3> - You complete the invocation by calling `request()`
|
||||||
|
|
||||||
Then, you can supply your custom `Saml2LogoutRequestResolver` in the DSL as follows:
|
[NOTE]
|
||||||
|
Support for OpenSAML 4 is coming.
|
||||||
[source,java]
|
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.
|
||||||
http
|
|
||||||
.saml2Logout((saml2) -> saml2
|
|
||||||
.logoutRequest((request) -> request
|
|
||||||
.logoutRequestResolver(myOpenSamlLogoutRequestResolver)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Customizing `<saml2:LogoutResponse>` Resolution
|
=== Customizing `<saml2:LogoutResponse>` Resolution
|
||||||
|
|
||||||
|
@ -1218,42 +1277,24 @@ To add other values, you can use delegation, like so:
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@Component
|
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
|
||||||
public class MyOpenSamlLogoutResponseResolver implements Saml2LogoutRequestResolver {
|
return (request, response, authentication) -> {
|
||||||
private final OpenSaml3LogoutResponseResolver logoutRequestResolver;
|
OpenSamlLogoutResponseBuilder builder = delegate.resolveLogoutResponse(request, response, authentication); <1>
|
||||||
|
if (checkOtherPrevailingConditions()) {
|
||||||
public MyOpenSamlLogoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
|
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
|
|
||||||
new DefaultRelyingPartyRegistrationResolver(registrations);
|
|
||||||
this.logoutResponseResolver = new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OpenSamlLogoutResponseBuilder resolveLogoutResponse(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
|
||||||
String name = ((Saml2AuthenticatedPrincipal) authentication.getPrincipal()).getFirstAttribute("CustomAttribute");
|
|
||||||
String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
|
|
||||||
OpenSamlLogoutResponseBuilder builder = logoutResponseResolver.resolveLogoutRequest(request, authentication); <1>
|
|
||||||
if (checkOtherPrevailingConditions()) {
|
|
||||||
builder.status(StatusCode.PARTIAL_LOGOUT); <2>
|
|
||||||
}
|
|
||||||
return builder;
|
|
||||||
}
|
}
|
||||||
}
|
builder.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
|
||||||
|
return builder.logoutResponse(); <3>
|
||||||
|
};
|
||||||
----
|
----
|
||||||
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
|
<1> - Spring Security applies default values to a `<saml2:LogoutResponse>`
|
||||||
<2> - Your application specifies customizations
|
<2> - Your application specifies customizations
|
||||||
|
<3> - You complete the invocation by calling `response()`
|
||||||
|
|
||||||
Then, you can supply your custom `Saml2LogoutResponseResolver` in the DSL as follows:
|
[NOTE]
|
||||||
|
Support for OpenSAML 4 is coming.
|
||||||
[source,java]
|
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.
|
||||||
http
|
|
||||||
.saml2Logout((saml2) -> saml2
|
|
||||||
.logoutRequest((request) -> request
|
|
||||||
.logoutRequestResolver(myOpenSamlLogoutRequestResolver)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Customizing `<saml2:LogoutRequest>` Validation
|
=== Customizing `<saml2:LogoutRequest>` Validation
|
||||||
|
|
||||||
|
@ -1262,37 +1303,16 @@ At this point, the validation is minimal, so you may be able to first delegate t
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@Component
|
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
|
||||||
public class MyOpenSamlLogoutRequestHandler implements LogoutHandler {
|
OpenSamlLogoutRequestHandler delegate = new OpenSamlLogoutRequestHandler(registrationResolver);
|
||||||
private final Saml2LogoutRequestHandler delegate;
|
return (request, response, authentication) -> {
|
||||||
|
|
||||||
public MyOpenSamlLogoutRequestHandler(RelyingPartyRegistrationRepository registrations) {
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
|
|
||||||
new DefaultRelyingPartyRegistrationResolver(registrations);
|
|
||||||
this.delegate = new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
|
||||||
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
|
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and principal name
|
||||||
LogoutRequest logoutRequest = // ... parse using OpenSAML
|
LogoutRequest logoutRequest = // ... parse using OpenSAML
|
||||||
// perform custom validation
|
// perform custom validation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
Then, you can supply your custom `LogoutHandler` in the DSL as follows:
|
|
||||||
|
|
||||||
[source,java]
|
|
||||||
----
|
|
||||||
http
|
|
||||||
.saml2Logout((saml2) -> saml2
|
|
||||||
.logoutRequest((request) -> request
|
|
||||||
.logoutRequestHandler(myOpenSamlLogoutRequestHandler)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Customizing `<saml2:LogoutResponse>` Validation
|
=== Customizing `<saml2:LogoutResponse>` Validation
|
||||||
|
|
||||||
To customize validation, you can implement your own `LogoutHandler`.
|
To customize validation, you can implement your own `LogoutHandler`.
|
||||||
|
@ -1300,49 +1320,12 @@ At this point, the validation is minimal, so you may be able to first delegate t
|
||||||
|
|
||||||
[source,java]
|
[source,java]
|
||||||
----
|
----
|
||||||
@Component
|
LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) {
|
||||||
public class MyOpenSamlLogoutResponseHandler implements LogoutHandler {
|
OpenSamlLogoutResponseHandler delegate = new OpenSamlLogoutResponseHandler(registrationResolver);
|
||||||
private final Saml2LogoutResponseHandler delegate;
|
return (request, response, authentication) -> {
|
||||||
|
|
||||||
public MyOpenSamlLogoutResponseHandler(RelyingPartyRegistrationRepository registrations) {
|
|
||||||
RelyingPartyRegistrationResolver relyingPartyRegistrationResolver =
|
|
||||||
new DefaultRelyingPartyRegistrationResolver(registrations);
|
|
||||||
this.delegate = new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
|
||||||
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
|
delegate.logout(request, response, authentication); // verify signature, issuer, destination, and status
|
||||||
LogoutResponse logoutResponse = // ... parse using OpenSAML
|
LogoutResponse logoutResponse = // ... parse using OpenSAML
|
||||||
// perform custom validation
|
// perform custom validation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
Then, you can supply your custom `LogoutHandler` in the DSL as follows:
|
|
||||||
|
|
||||||
[source,java]
|
|
||||||
----
|
|
||||||
http
|
|
||||||
.saml2Logout((saml2) -> saml2
|
|
||||||
.logoutResponse((response) -> response
|
|
||||||
.logoutResponseHandler(myOpenSamlLogoutResponseHandler)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
----
|
|
||||||
|
|
||||||
=== Customizing `<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)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
----
|
|
||||||
|
|
Loading…
Reference in New Issue