Skip to content

Commit 93d4fd3

Browse files
Add SAML 2.0 Single Logout XML Support
Closes gh-10842
1 parent 73f8393 commit 93d4fd3

18 files changed

+1336
-6
lines changed

config/src/main/java/org/springframework/security/config/Elements.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,6 @@ public abstract class Elements {
142142

143143
public static final String SAML2_LOGIN = "saml2-login";
144144

145+
public static final String SAML2_LOGOUT = "saml2-logout";
146+
145147
}

config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ final class AuthenticationConfigBuilder {
164164
@SuppressWarnings("rawtypes")
165165
private ManagedList logoutHandlers;
166166

167+
private BeanMetadataElement logoutSuccessHandler;
168+
167169
private BeanDefinition loginPageGenerationFilter;
168170

169171
private BeanDefinition logoutPageGenerationFilter;
@@ -210,6 +212,12 @@ final class AuthenticationConfigBuilder {
210212

211213
private String saml2AuthenticationRequestFilterId;
212214

215+
private String saml2LogoutFilterId;
216+
217+
private String saml2LogoutRequestFilterId;
218+
219+
private String saml2LogoutResponseFilterId;
220+
213221
private boolean oauth2ClientEnabled;
214222

215223
private BeanDefinition authorizationRequestRedirectFilter;
@@ -250,6 +258,7 @@ final class AuthenticationConfigBuilder {
250258
createX509Filter(authenticationManager);
251259
createJeeFilter(authenticationManager);
252260
createLogoutFilter();
261+
createSaml2LogoutFilter();
253262
createLoginPageFilterIfNeeded();
254263
createUserDetailsServiceFactory();
255264
createExceptionTranslationFilter();
@@ -720,9 +729,33 @@ void createLogoutFilter() {
720729
this.rememberMeServicesId, this.csrfLogoutHandler);
721730
this.logoutFilter = logoutParser.parse(logoutElt, this.pc);
722731
this.logoutHandlers = logoutParser.getLogoutHandlers();
732+
this.logoutSuccessHandler = logoutParser.getLogoutSuccessHandler();
723733
}
724734
}
725735

736+
private void createSaml2LogoutFilter() {
737+
Element saml2LogoutElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.SAML2_LOGOUT);
738+
if (saml2LogoutElt == null) {
739+
return;
740+
}
741+
Saml2LogoutBeanDefinitionParser parser = new Saml2LogoutBeanDefinitionParser(this.logoutHandlers,
742+
this.logoutSuccessHandler);
743+
parser.parse(saml2LogoutElt, this.pc);
744+
BeanDefinition saml2LogoutFilter = parser.getLogoutFilter();
745+
BeanDefinition saml2LogoutRequestFilter = parser.getLogoutRequestFilter();
746+
BeanDefinition saml2LogoutResponseFilter = parser.getLogoutResponseFilter();
747+
this.saml2LogoutFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutFilter);
748+
this.saml2LogoutRequestFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutRequestFilter);
749+
this.saml2LogoutResponseFilterId = this.pc.getReaderContext().generateBeanName(saml2LogoutResponseFilter);
750+
751+
// register the component
752+
this.pc.registerBeanComponent(new BeanComponentDefinition(saml2LogoutFilter, this.saml2LogoutFilterId));
753+
this.pc.registerBeanComponent(
754+
new BeanComponentDefinition(saml2LogoutRequestFilter, this.saml2LogoutRequestFilterId));
755+
this.pc.registerBeanComponent(
756+
new BeanComponentDefinition(saml2LogoutResponseFilter, this.saml2LogoutResponseFilterId));
757+
}
758+
726759
@SuppressWarnings({ "rawtypes", "unchecked" })
727760
ManagedList getLogoutHandlers() {
728761
if (this.logoutHandlers == null && this.rememberMeProviderRef != null) {
@@ -968,6 +1001,14 @@ List<OrderDecorator> getFilters() {
9681001
filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2AuthenticationRequestFilterId),
9691002
SecurityFilters.SAML2_AUTHENTICATION_REQUEST_FILTER));
9701003
}
1004+
if (this.saml2LogoutFilterId != null) {
1005+
filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutFilterId),
1006+
SecurityFilters.SAML2_LOGOUT_FILTER));
1007+
filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutRequestFilterId),
1008+
SecurityFilters.SAML2_LOGOUT_REQUEST_FILTER));
1009+
filters.add(new OrderDecorator(new RuntimeBeanReference(this.saml2LogoutResponseFilterId),
1010+
SecurityFilters.SAML2_LOGOUT_RESPONSE_FILTER));
1011+
}
9711012
filters.add(new OrderDecorator(this.etf, SecurityFilters.EXCEPTION_TRANSLATION_FILTER));
9721013
return filters;
9731014
}

config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
5959

6060
private boolean csrfEnabled;
6161

62+
private BeanMetadataElement logoutSuccessHandler;
63+
6264
LogoutBeanDefinitionParser(String loginPageUrl, String rememberMeServices, BeanMetadataElement csrfLogoutHandler) {
6365
this.defaultLogoutUrl = loginPageUrl + "?logout";
6466
this.rememberMeServices = rememberMeServices;
@@ -98,6 +100,7 @@ public BeanDefinition parse(Element element, ParserContext pc) {
98100
pc.extractSource(element));
99101
}
100102
builder.addConstructorArgReference(successHandlerRef);
103+
this.logoutSuccessHandler = new RuntimeBeanReference(successHandlerRef);
101104
}
102105
else {
103106
// Use the logout URL if no handler set
@@ -137,4 +140,8 @@ ManagedList<BeanMetadataElement> getLogoutHandlers() {
137140
return this.logoutHandlers;
138141
}
139142

143+
BeanMetadataElement getLogoutSuccessHandler() {
144+
return this.logoutSuccessHandler;
145+
}
146+
140147
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.http;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Objects;
22+
import java.util.function.Predicate;
23+
24+
import javax.servlet.http.HttpServletRequest;
25+
26+
import org.w3c.dom.Element;
27+
28+
import org.springframework.beans.BeanMetadataElement;
29+
import org.springframework.beans.factory.config.BeanDefinition;
30+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
31+
import org.springframework.beans.factory.support.ManagedList;
32+
import org.springframework.beans.factory.xml.BeanDefinitionParser;
33+
import org.springframework.beans.factory.xml.ParserContext;
34+
import org.springframework.security.core.Authentication;
35+
import org.springframework.security.core.context.SecurityContextHolder;
36+
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
37+
import org.springframework.security.saml2.provider.service.web.DefaultRelyingPartyRegistrationResolver;
38+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter;
39+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutResponseFilter;
40+
import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler;
41+
import org.springframework.security.web.authentication.logout.LogoutFilter;
42+
import org.springframework.security.web.authentication.logout.LogoutSuccessEventPublishingLogoutHandler;
43+
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
44+
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
45+
import org.springframework.security.web.util.matcher.AndRequestMatcher;
46+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
47+
import org.springframework.security.web.util.matcher.RequestMatcher;
48+
import org.springframework.util.CollectionUtils;
49+
import org.springframework.util.StringUtils;
50+
51+
/**
52+
* SAML 2.0 Single Logout {@link BeanDefinitionParser}
53+
*
54+
* @author Marcus da Coregio
55+
* @since 5.7
56+
*/
57+
final class Saml2LogoutBeanDefinitionParser implements BeanDefinitionParser {
58+
59+
private static final String ATT_LOGOUT_REQUEST_URL = "logout-request-url";
60+
61+
private static final String ATT_LOGOUT_RESPONSE_URL = "logout-response-url";
62+
63+
private static final String ATT_LOGOUT_URL = "logout-url";
64+
65+
private List<BeanMetadataElement> logoutHandlers;
66+
67+
private String logoutUrl = "/logout";
68+
69+
private String logoutRequestUrl = "/logout/saml2/slo";
70+
71+
private String logoutResponseUrl = "/logout/saml2/slo";
72+
73+
private BeanMetadataElement logoutSuccessHandler;
74+
75+
private BeanDefinition logoutRequestFilter;
76+
77+
private BeanDefinition logoutResponseFilter;
78+
79+
private BeanDefinition logoutFilter;
80+
81+
Saml2LogoutBeanDefinitionParser(ManagedList<BeanMetadataElement> logoutHandlers,
82+
BeanMetadataElement logoutSuccessHandler) {
83+
this.logoutHandlers = logoutHandlers;
84+
this.logoutSuccessHandler = logoutSuccessHandler;
85+
}
86+
87+
@Override
88+
public BeanDefinition parse(Element element, ParserContext pc) {
89+
String logoutUrl = element.getAttribute(ATT_LOGOUT_URL);
90+
if (StringUtils.hasText(logoutUrl)) {
91+
this.logoutUrl = logoutUrl;
92+
}
93+
String logoutRequestUrl = element.getAttribute(ATT_LOGOUT_REQUEST_URL);
94+
if (StringUtils.hasText(logoutRequestUrl)) {
95+
this.logoutRequestUrl = logoutRequestUrl;
96+
}
97+
String logoutResponseUrl = element.getAttribute(ATT_LOGOUT_RESPONSE_URL);
98+
if (StringUtils.hasText(logoutResponseUrl)) {
99+
this.logoutResponseUrl = logoutResponseUrl;
100+
}
101+
WebConfigUtils.validateHttpRedirect(this.logoutUrl, pc, element);
102+
WebConfigUtils.validateHttpRedirect(this.logoutRequestUrl, pc, element);
103+
WebConfigUtils.validateHttpRedirect(this.logoutResponseUrl, pc, element);
104+
if (CollectionUtils.isEmpty(this.logoutHandlers)) {
105+
this.logoutHandlers = createDefaultLogoutHandlers();
106+
}
107+
if (this.logoutSuccessHandler == null) {
108+
this.logoutSuccessHandler = createDefaultLogoutSuccessHandler();
109+
}
110+
BeanMetadataElement relyingPartyRegistrationRepository = Saml2LogoutBeanDefinitionParserUtils
111+
.getRelyingPartyRegistrationRepository(element);
112+
BeanMetadataElement registrations = BeanDefinitionBuilder
113+
.rootBeanDefinition(DefaultRelyingPartyRegistrationResolver.class)
114+
.addConstructorArgValue(relyingPartyRegistrationRepository).getBeanDefinition();
115+
BeanMetadataElement logoutResponseResolver = Saml2LogoutBeanDefinitionParserUtils
116+
.getLogoutResponseResolver(element, registrations);
117+
BeanMetadataElement logoutRequestValidator = Saml2LogoutBeanDefinitionParserUtils
118+
.getLogoutRequestValidator(element);
119+
BeanMetadataElement logoutRequestMatcher = createSaml2LogoutRequestMatcher();
120+
this.logoutRequestFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutRequestFilter.class)
121+
.addConstructorArgValue(registrations).addConstructorArgValue(logoutRequestValidator)
122+
.addConstructorArgValue(logoutResponseResolver).addConstructorArgValue(this.logoutHandlers)
123+
.addPropertyValue("logoutRequestMatcher", logoutRequestMatcher).getBeanDefinition();
124+
BeanMetadataElement logoutResponseValidator = Saml2LogoutBeanDefinitionParserUtils
125+
.getLogoutResponseValidator(element);
126+
BeanMetadataElement logoutRequestRepository = Saml2LogoutBeanDefinitionParserUtils
127+
.getLogoutRequestRepository(element);
128+
BeanMetadataElement logoutResponseMatcher = createSaml2LogoutResponseMatcher();
129+
this.logoutResponseFilter = BeanDefinitionBuilder.rootBeanDefinition(Saml2LogoutResponseFilter.class)
130+
.addConstructorArgValue(registrations).addConstructorArgValue(logoutResponseValidator)
131+
.addConstructorArgValue(this.logoutSuccessHandler)
132+
.addPropertyValue("logoutRequestMatcher", logoutResponseMatcher)
133+
.addPropertyValue("logoutRequestRepository", logoutRequestRepository).getBeanDefinition();
134+
BeanMetadataElement logoutRequestResolver = Saml2LogoutBeanDefinitionParserUtils
135+
.getLogoutRequestResolver(element, registrations);
136+
BeanMetadataElement saml2LogoutRequestSuccessHandler = BeanDefinitionBuilder
137+
.rootBeanDefinition(Saml2RelyingPartyInitiatedLogoutSuccessHandler.class)
138+
.addConstructorArgValue(logoutRequestResolver).getBeanDefinition();
139+
this.logoutFilter = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class)
140+
.addConstructorArgValue(saml2LogoutRequestSuccessHandler).addConstructorArgValue(this.logoutHandlers)
141+
.addPropertyValue("logoutRequestMatcher", createLogoutRequestMatcher()).getBeanDefinition();
142+
return null;
143+
}
144+
145+
private static List<BeanMetadataElement> createDefaultLogoutHandlers() {
146+
List<BeanMetadataElement> handlers = new ManagedList<>();
147+
handlers.add(BeanDefinitionBuilder.rootBeanDefinition(SecurityContextLogoutHandler.class).getBeanDefinition());
148+
handlers.add(BeanDefinitionBuilder.rootBeanDefinition(LogoutSuccessEventPublishingLogoutHandler.class)
149+
.getBeanDefinition());
150+
return handlers;
151+
}
152+
153+
private static BeanMetadataElement createDefaultLogoutSuccessHandler() {
154+
return BeanDefinitionBuilder.rootBeanDefinition(SimpleUrlLogoutSuccessHandler.class)
155+
.addPropertyValue("defaultTargetUrl", "/login?logout").getBeanDefinition();
156+
}
157+
158+
private BeanMetadataElement createLogoutRequestMatcher() {
159+
BeanMetadataElement logoutMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
160+
.addConstructorArgValue(this.logoutUrl).addConstructorArgValue("POST").getBeanDefinition();
161+
BeanMetadataElement saml2Matcher = BeanDefinitionBuilder.rootBeanDefinition(Saml2RequestMatcher.class)
162+
.getBeanDefinition();
163+
return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
164+
.addConstructorArgValue(toManagedList(logoutMatcher, saml2Matcher)).getBeanDefinition();
165+
}
166+
167+
private BeanMetadataElement createSaml2LogoutRequestMatcher() {
168+
BeanMetadataElement logoutRequestMatcher = BeanDefinitionBuilder.rootBeanDefinition(AntPathRequestMatcher.class)
169+
.addConstructorArgValue(this.logoutRequestUrl).getBeanDefinition();
170+
BeanMetadataElement saml2RequestMatcher = BeanDefinitionBuilder
171+
.rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLRequest")
172+
.getBeanDefinition();
173+
return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
174+
.addConstructorArgValue(toManagedList(logoutRequestMatcher, saml2RequestMatcher)).getBeanDefinition();
175+
}
176+
177+
private BeanMetadataElement createSaml2LogoutResponseMatcher() {
178+
BeanMetadataElement logoutResponseMatcher = BeanDefinitionBuilder
179+
.rootBeanDefinition(AntPathRequestMatcher.class).addConstructorArgValue(this.logoutResponseUrl)
180+
.getBeanDefinition();
181+
BeanMetadataElement saml2ResponseMatcher = BeanDefinitionBuilder
182+
.rootBeanDefinition(ParameterRequestMatcher.class).addConstructorArgValue("SAMLResponse")
183+
.getBeanDefinition();
184+
return BeanDefinitionBuilder.rootBeanDefinition(AndRequestMatcher.class)
185+
.addConstructorArgValue(toManagedList(logoutResponseMatcher, saml2ResponseMatcher)).getBeanDefinition();
186+
}
187+
188+
private static List<BeanMetadataElement> toManagedList(BeanMetadataElement... elements) {
189+
List<BeanMetadataElement> managedList = new ManagedList<>();
190+
managedList.addAll(Arrays.asList(elements));
191+
return managedList;
192+
}
193+
194+
BeanDefinition getLogoutRequestFilter() {
195+
return this.logoutRequestFilter;
196+
}
197+
198+
BeanDefinition getLogoutResponseFilter() {
199+
return this.logoutResponseFilter;
200+
}
201+
202+
BeanDefinition getLogoutFilter() {
203+
return this.logoutFilter;
204+
}
205+
206+
private static class ParameterRequestMatcher implements RequestMatcher {
207+
208+
Predicate<String> test = Objects::nonNull;
209+
210+
String name;
211+
212+
ParameterRequestMatcher(String name) {
213+
this.name = name;
214+
}
215+
216+
@Override
217+
public boolean matches(HttpServletRequest request) {
218+
return this.test.test(request.getParameter(this.name));
219+
}
220+
221+
}
222+
223+
private static class Saml2RequestMatcher implements RequestMatcher {
224+
225+
@Override
226+
public boolean matches(HttpServletRequest request) {
227+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
228+
if (authentication == null) {
229+
return false;
230+
}
231+
return authentication.getPrincipal() instanceof Saml2AuthenticatedPrincipal;
232+
}
233+
234+
}
235+
236+
}

0 commit comments

Comments
 (0)