5 changed files with 119 additions and 1314 deletions
@ -1,637 +0,0 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue