org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer Maven / Gradle / Ivy
/*
* Copyright 2002-2023 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 jakarta.servlet.http.HttpServletRequest;
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.SecurityContextHolderStrategy;
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.authentication.logout.HttpSessionLogoutRequestRepository;
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.OpenSamlLogoutRequestValidatorParametersResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseResolver;
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.csrf.CsrfLogoutHandler;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Adds SAML 2.0 logout support.
*
* Security Filters
*
* The following Filters are populated
*
*
* - {@link LogoutFilter}
* - {@link Saml2LogoutRequestFilter}
* - {@link Saml2LogoutResponseFilter}
*
*
*
* The following configuration options are available:
*
*
* - {@link #logoutUrl} - The URL to to process SAML 2.0 Logout
* - {@link LogoutRequestConfigurer#logoutRequestValidator} - The
* {@link AuthenticationManager} for authenticating SAML 2.0 Logout Requests
* - {@link LogoutRequestConfigurer#logoutRequestResolver} - The
* {@link Saml2LogoutRequestResolver} for creating SAML 2.0 Logout Requests
* - {@link LogoutRequestConfigurer#logoutRequestRepository} - The
* {@link Saml2LogoutRequestRepository} for storing SAML 2.0 Logout Requests
* - {@link LogoutResponseConfigurer#logoutResponseValidator} - The
* {@link AuthenticationManager} for authenticating SAML 2.0 Logout Responses
* - {@link LogoutResponseConfigurer#logoutResponseResolver} - The
* {@link Saml2LogoutResponseResolver} for creating SAML 2.0 Logout Responses
*
*
* Shared Objects Created
*
* No shared Objects are created
*
* Shared Objects Used
*
* Uses {@link CsrfTokenRepository} to add the {@link CsrfLogoutHandler}.
*
* @author Josh Cummings
* @since 5.6
* @see Saml2LogoutConfigurer
*/
public final class Saml2LogoutConfigurer>
extends AbstractHttpConfigurer, H> {
private ApplicationContext context;
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
private String logoutUrl = "/logout";
private List logoutHandlers = new ArrayList<>();
private LogoutSuccessHandler logoutSuccessHandler;
private LogoutRequestConfigurer logoutRequestConfigurer;
private LogoutResponseConfigurer logoutResponseConfigurer;
/**
* Creates a new instance
* @see HttpSecurity#logout()
*/
public Saml2LogoutConfigurer(ApplicationContext context) {
this.context = context;
this.logoutHandlers.add(new SecurityContextLogoutHandler());
this.logoutHandlers.add(new LogoutSuccessEventPublishingLogoutHandler());
SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
logoutSuccessHandler.setDefaultTargetUrl("/login?logout");
this.logoutSuccessHandler = logoutSuccessHandler;
this.logoutRequestConfigurer = new LogoutRequestConfigurer();
this.logoutResponseConfigurer = new LogoutResponseConfigurer();
}
/**
* The URL by which the relying or asserting party can trigger logout.
*
*
* The Relying Party triggers logout by POSTing to the endpoint. The Asserting Party
* triggers logout based on what is specified by
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @param logoutUrl the URL that will invoke logout
* @return the {@link LogoutConfigurer} for further customizations
* @see LogoutConfigurer#logoutUrl(String)
* @see HttpSecurity#csrf()
*/
public Saml2LogoutConfigurer logoutUrl(String logoutUrl) {
this.logoutUrl = logoutUrl;
return this;
}
/**
* Sets the {@link RelyingPartyRegistrationRepository} of relying parties, each party
* representing a service provider, SP and this host, and identity provider, IDP pair
* that communicate with each other.
* @param repo the repository of relying parties
* @return the {@link Saml2LogoutConfigurer} for further customizations
*/
public Saml2LogoutConfigurer relyingPartyRegistrationRepository(RelyingPartyRegistrationRepository repo) {
this.relyingPartyRegistrationRepository = repo;
return this;
}
/**
* Get configurer for SAML 2.0 Logout Request components
* @return the {@link LogoutRequestConfigurer} for further customizations
* @deprecated For removal in 7.0. Use {@link #logoutRequest(Customizer)} or
* {@code logoutRequest(Customizer.withDefaults())} to stick with defaults. See the
* documentation
* for more details.
*/
@Deprecated(since = "6.1", forRemoval = true)
public LogoutRequestConfigurer logoutRequest() {
return this.logoutRequestConfigurer;
}
/**
* Configures SAML 2.0 Logout Request components
* @param logoutRequestConfigurerCustomizer the {@link Customizer} to provide more
* options for the {@link LogoutRequestConfigurer}
* @return the {@link Saml2LogoutConfigurer} for further customizations
*/
public Saml2LogoutConfigurer logoutRequest(
Customizer logoutRequestConfigurerCustomizer) {
logoutRequestConfigurerCustomizer.customize(this.logoutRequestConfigurer);
return this;
}
/**
* Get configurer for SAML 2.0 Logout Response components
* @return the {@link LogoutResponseConfigurer} for further customizations
* @deprecated For removal in 7.0. Use {@link #logoutResponse(Customizer)} or
* {@code logoutResponse(Customizer.withDefaults())} to stick with defaults. See the
* documentation
* for more details.
*/
@Deprecated(since = "6.1", forRemoval = true)
public LogoutResponseConfigurer logoutResponse() {
return this.logoutResponseConfigurer;
}
/**
* Configures SAML 2.0 Logout Response components
* @param logoutResponseConfigurerCustomizer the {@link Customizer} to provide more
* options for the {@link LogoutResponseConfigurer}
* @return the {@link Saml2LogoutConfigurer} for further customizations
*/
public Saml2LogoutConfigurer logoutResponse(
Customizer logoutResponseConfigurerCustomizer) {
logoutResponseConfigurerCustomizer.customize(this.logoutResponseConfigurer);
return this;
}
/**
* {@inheritDoc}
*/
@Override
public void configure(H http) throws Exception {
LogoutConfigurer logout = http.getConfigurer(LogoutConfigurer.class);
if (logout != null) {
this.logoutHandlers = logout.getLogoutHandlers();
this.logoutSuccessHandler = logout.getLogoutSuccessHandler();
}
RelyingPartyRegistrationRepository registrations = getRelyingPartyRegistrationRepository(http);
http.addFilterBefore(createLogoutRequestProcessingFilter(registrations), CsrfFilter.class);
http.addFilterBefore(createLogoutResponseProcessingFilter(registrations), CsrfFilter.class);
http.addFilterBefore(createRelyingPartyLogoutFilter(registrations), LogoutFilter.class);
}
private RelyingPartyRegistrationRepository getRelyingPartyRegistrationRepository(H http) {
if (this.relyingPartyRegistrationRepository != null) {
return this.relyingPartyRegistrationRepository;
}
Saml2LoginConfigurer login = http.getConfigurer(Saml2LoginConfigurer.class);
if (login != null) {
this.relyingPartyRegistrationRepository = login.relyingPartyRegistrationRepository(http);
}
else {
this.relyingPartyRegistrationRepository = getBeanOrNull(RelyingPartyRegistrationRepository.class);
}
return this.relyingPartyRegistrationRepository;
}
private Saml2LogoutRequestFilter createLogoutRequestProcessingFilter(
RelyingPartyRegistrationRepository registrations) {
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
Saml2LogoutResponseResolver logoutResponseResolver = createSaml2LogoutResponseResolver(registrations);
RequestMatcher requestMatcher = createLogoutRequestMatcher();
OpenSamlLogoutRequestValidatorParametersResolver parameters = new OpenSamlLogoutRequestValidatorParametersResolver(
registrations);
parameters.setRequestMatcher(requestMatcher);
Saml2LogoutRequestFilter filter = new Saml2LogoutRequestFilter(parameters,
this.logoutRequestConfigurer.logoutRequestValidator(), logoutResponseResolver, logoutHandlers);
filter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
return postProcess(filter);
}
private Saml2LogoutResponseFilter createLogoutResponseProcessingFilter(
RelyingPartyRegistrationRepository registrations) {
Saml2LogoutResponseFilter logoutResponseFilter = new Saml2LogoutResponseFilter(registrations,
this.logoutResponseConfigurer.logoutResponseValidator(), this.logoutSuccessHandler);
logoutResponseFilter.setLogoutRequestMatcher(createLogoutResponseMatcher());
logoutResponseFilter.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository);
return postProcess(logoutResponseFilter);
}
private Saml2RelyingPartyInitiatedLogoutFilter createRelyingPartyLogoutFilter(
RelyingPartyRegistrationRepository registrations) {
LogoutHandler[] logoutHandlers = this.logoutHandlers.toArray(new LogoutHandler[0]);
Saml2RelyingPartyInitiatedLogoutSuccessHandler logoutRequestSuccessHandler = createSaml2LogoutRequestSuccessHandler(
registrations);
logoutRequestSuccessHandler.setLogoutRequestRepository(this.logoutRequestConfigurer.logoutRequestRepository);
Saml2RelyingPartyInitiatedLogoutFilter logoutFilter = new Saml2RelyingPartyInitiatedLogoutFilter(
logoutRequestSuccessHandler, logoutHandlers);
logoutFilter.setLogoutRequestMatcher(createLogoutMatcher());
return postProcess(logoutFilter);
}
private RequestMatcher createLogoutMatcher() {
RequestMatcher logout = new AntPathRequestMatcher(this.logoutUrl, "POST");
RequestMatcher saml2 = new Saml2RequestMatcher(getSecurityContextHolderStrategy());
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(
RelyingPartyRegistrationRepository registrations) {
Saml2LogoutRequestResolver logoutRequestResolver = this.logoutRequestConfigurer
.logoutRequestResolver(registrations);
return new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver);
}
private Saml2LogoutResponseResolver createSaml2LogoutResponseResolver(
RelyingPartyRegistrationRepository registrations) {
return this.logoutResponseConfigurer.logoutResponseResolver(registrations);
}
private C getBeanOrNull(Class clazz) {
if (this.context == null) {
return null;
}
if (this.context.getBeanNamesForType(clazz).length == 0) {
return null;
}
return this.context.getBean(clazz);
}
/**
* A configurer for SAML 2.0 LogoutRequest components
*/
public final class LogoutRequestConfigurer {
private String logoutUrl = "/logout/saml2/slo";
private Saml2LogoutRequestValidator logoutRequestValidator;
private Saml2LogoutRequestResolver logoutRequestResolver;
private Saml2LogoutRequestRepository logoutRequestRepository = new HttpSessionLogoutRequestRepository();
LogoutRequestConfigurer() {
}
/**
* The URL by which the asserting party can send a SAML 2.0 Logout Request
*
*
* The Asserting Party should use whatever HTTP method specified in
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @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;
}
/**
* @deprecated For removal in 7.0. Use {@link #logoutRequest(Customizer)} or
* {@code logoutRequest(Customizer.withDefaults())} to stick with defaults. See
* the documentation
* for more details.
*/
@Deprecated(since = "6.1", forRemoval = true)
public Saml2LogoutConfigurer and() {
return Saml2LogoutConfigurer.this;
}
private Saml2LogoutRequestValidator logoutRequestValidator() {
if (this.logoutRequestValidator == null) {
return new OpenSamlLogoutRequestValidator();
}
return this.logoutRequestValidator;
}
private Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
if (this.logoutRequestResolver != null) {
return this.logoutRequestResolver;
}
return new OpenSaml4LogoutRequestResolver(registrations);
}
}
public final class LogoutResponseConfigurer {
private String logoutUrl = "/logout/saml2/slo";
private Saml2LogoutResponseValidator logoutResponseValidator;
private Saml2LogoutResponseResolver logoutResponseResolver;
LogoutResponseConfigurer() {
}
/**
* The URL by which the asserting party can send a SAML 2.0 Logout Response
*
*
* The Asserting Party should use whatever HTTP method specified in
* {@link RelyingPartyRegistration#getSingleLogoutServiceBindings()}.
* @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;
}
/**
* @deprecated For removal in 7.0. Use {@link #logoutResponse(Customizer)} or
* {@code logoutResponse(Customizer.withDefaults())} to stick with defaults. See
* the documentation
* for more details.
*/
@Deprecated(since = "6.1", forRemoval = true)
public Saml2LogoutConfigurer and() {
return Saml2LogoutConfigurer.this;
}
private Saml2LogoutResponseValidator logoutResponseValidator() {
if (this.logoutResponseValidator == null) {
return new OpenSamlLogoutResponseValidator();
}
return this.logoutResponseValidator;
}
private Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
if (this.logoutResponseResolver == null) {
return new OpenSaml4LogoutResponseResolver(registrations);
}
return this.logoutResponseResolver;
}
}
private static class Saml2RequestMatcher implements RequestMatcher {
private final SecurityContextHolderStrategy securityContextHolderStrategy;
Saml2RequestMatcher(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
@Override
public boolean matches(HttpServletRequest request) {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
if (authentication == null) {
return false;
}
return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
}
}
private static class ParameterRequestMatcher implements RequestMatcher {
Predicate test = Objects::nonNull;
String name;
ParameterRequestMatcher(String name) {
this.name = name;
}
@Override
public boolean matches(HttpServletRequest request) {
return this.test.test(request.getParameter(this.name));
}
}
private static class Saml2RelyingPartyInitiatedLogoutFilter extends LogoutFilter {
Saml2RelyingPartyInitiatedLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... handlers) {
super(logoutSuccessHandler, handlers);
}
}
}