1
0
mirror of synced 2026-05-22 21:33:16 +00:00

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
@@ -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>
@@ -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;
}
@@ -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));
@@ -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) {
}
}
}
@@ -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;
}
}
@@ -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);
}
}
}