Skip to content

Commit 1125b41

Browse files
committed
Add Saml2LogoutFilter
1 parent ddf8e7a commit 1125b41

File tree

10 files changed

+278
-80
lines changed

10 files changed

+278
-80
lines changed

docs/manual/src/docs/asciidoc/_includes/servlet/saml2/saml2-login.adoc

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,34 +1087,33 @@ RelyingPartyRegistrationRepository registrations() {
10871087
@Bean
10881088
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
10891089
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1090+
LogoutHandler logoutHandler = logoutHandler(registrationResolver);
1091+
LogoutSuccessHandler successHandler = new SimpleUrlLogoutSuccessHandler();
10901092
10911093
http
10921094
.authorizeRequests((authorize) -> authorize
10931095
.anyRequest().authenticated()
10941096
)
10951097
.saml2Login(withDefaults())
1096-
.logout((logout) -> logout.addLogoutHandler(logoutHandler(registrationResolver)))
1097-
.addFilterBefore(filter(registrationResolver), LogoutFilter.class)
1098-
.csrf((csrf) -> csrf.ignoringRequestMatchers(hasLogoutResponse("/logout", "POST")));
1098+
.addFilterBefore(new Saml2LogoutFilter(successHandler, logoutHandler), CsrfFilter.class)
1099+
.addFilterBefore(logoutRequestFilter(registrationResolver), LogoutFilter.class);
10991100
11001101
return http.build();
11011102
}
11021103
1103-
private RequestMatcher hasLogoutResponse(String endpoint, String method) {
1104-
AntPathRequestMatcher logout = new AndPathRequestMatcher(endpoint, method);
1105-
return (request) -> logout.matches(request) && request.getParameter("SAMLResponse") != null;
1106-
}
1107-
1108-
private Filter filter(RelyingPartyRegistrationResolver registrationResolver) { <2>
1104+
private Filter logoutRequestFilter(RelyingPartyRegistrationResolver registrationResolver) { <2>
11091105
OpenSamlLogoutRequestResolver delegate = new OpenSamlLogoutRequestResolver(registrationResolver);
11101106
Saml2LogoutRequestResolver logoutRequestResolver = (request, authentication) ->
11111107
delegate.resolveLogoutRequest(request, authentication)
11121108
.logoutRequest((logoutRequest) -> logoutRequest.setIssueInstant(DateTime.now()));
1113-
return new Saml2RelyingPartyInitiatedLogoutFilter(logoutRequestResolver);
1109+
return new Saml2LogoutRequestFilter(logoutRequestResolver);
11141110
}
11151111
11161112
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
1117-
return new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver);
1113+
return new CompositeLogoutHandler(
1114+
new OpenSamlLogoutResponseHandler(relyingPartyRegistrationResolver),
1115+
new SecurityContextLogoutHandler(),
1116+
new LogoutSuccessEventPublishingLogoutHandler());
11181117
}
11191118
----
11201119
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
@@ -1156,36 +1155,32 @@ RelyingPartyRegistrationRepository registrations() {
11561155
@Bean
11571156
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
11581157
RelyingPartyRegistrationResolver registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations);
1158+
LogoutHandler logoutHandler = logoutHandler(registrationResolver);
1159+
LogoutSuccessHandler successHandler = logoutSuccessHandler(registrationResolver);
11591160
11601161
http
11611162
.authorizeRequests((authorize) -> authorize
11621163
.anyRequest().authenticated()
11631164
)
11641165
.saml2Login(withDefaults())
1165-
.logout((logout) -> logout
1166-
.addLogoutHandler(logoutHandler(registrationResolver))
1167-
.logoutSuccessHandler(logoutSuccessHandler(registrationResolver))
1168-
)
1169-
.csrf((csrf) -> csrf.ignoringRequestMatchers(hasLogoutRequest("/logout", "POST")));
1166+
.addFilterBefore(new Saml2LogoutFilter(successHandler, logoutHandler), CsrfFilter.class);
11701167
11711168
return http.build();
11721169
}
11731170
1174-
private RequestMatcher hasLogoutRequest(String endpoint, String method) {
1175-
AntPathRequestMatcher logout = new AndPathRequestMatcher(endpoint, method);
1176-
return (request) -> logout.matches(request) && request.getParameter("SAMLRequest") != null;
1177-
}
1178-
11791171
private LogoutHandler logoutHandler(RelyingPartyRegistrationResolver registrationResolver) { <2>
1180-
return new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver);
1172+
return new CompositeLogoutHandler(
1173+
new OpenSamlLogoutRequestHandler(relyingPartyRegistrationResolver),
1174+
new SecurityContextLogoutHandler(),
1175+
new LogoutSuccessEventPublishingLogoutHandler());
11811176
}
11821177
11831178
private LogoutSuccessHandler logoutSuccessHandler(RelyingPartyRegistrationResolver registrationResolver) { <3>
11841179
OpenSamlLogoutResponseResolver delegate = new OpenSamlLogoutResponseResolver(registrationResolver);
11851180
Saml2LogoutResponsetResolver logoutResponseResolver = (request, authentication) ->
11861181
delegate.resolveLogoutResponse(request, authentication)
11871182
.logoutResponse((logoutResponse) -> logoutResponse.setIssueInstant(DateTime.now()));
1188-
return new Saml2AssertingPartyInitiatedLogoutSuccessHandler(logoutResponseResolver);
1183+
return new Saml2ResponseLogoutSuccessHandler(logoutResponseResolver);
11891184
}
11901185
----
11911186
<1> - First, add your signing key to the `RelyingPartyRegistration` instance or to <<servlet-saml2login-rpr-duplicated,multiple instances>>
@@ -1212,40 +1207,26 @@ In the event that you need to support both logout flows, you can combine the abo
12121207

12131208
There are two default endpoints that Spring Security's SAML 2.0 Single Logout support exposes:
12141209
* `/saml2/logout` - the endpoint for initiating single logout with an asserting party
1215-
* `/logout` - the endpoint for receiving logout requests and responses from an asserting party
1210+
* `/logout/saml2` - the endpoint for receiving logout requests and responses from an asserting party
12161211

12171212
Because the user is already logged in, the `registrationId` is already known.
12181213
For this reason, `+{registrationId}+` is not part of these URLs by default.
12191214

12201215
The first URL is not customizable at this point since this is not a URL that gets configured with the asserting party.
12211216
As such the need to customize this endpoint is minimal, though this can be added to the support down the road.
12221217

1223-
The second URL is customizable through Spring Security's <<jc-logout,general-purpose logout support>>.
1218+
The second URL is customizable in the `Saml2LogoutFilter`.
12241219

12251220
For example, if you are migrating your existing relying party over to Spring Security, your asserting party may already be pointing to `GET /SLOService.saml2`.
12261221
To reduce changes in configuration for the asserting party, you can configure `logout` in the DSL like so:
12271222

12281223
[source,java]
12291224
----
1225+
Saml2LogoutFilter filter = new Saml2LogoutFilter(logoutSuccessHandler, logoutHandler);
1226+
filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "GET"));
12301227
http
12311228
// ...
1232-
.logout((logout) -> logout
1233-
.logoutRequestMatcher(new AntPathRequestMatcher("/SLOService.saml2", "POST"))
1234-
// add logout handlers
1235-
);
1236-
----
1237-
1238-
In redirect circumstances it's important to maintain your defense mechanisms for CSRF Logout.
1239-
You can configure your SAML 2.0 logout as a GET by only matching when there is a `SAMLRequest` (or `SAMLResponse`) present, like so:
1240-
1241-
[source,java]
1242-
----
1243-
http
1244-
// ...
1245-
.logout((logout) -> logout
1246-
.logoutRequestMatcher(hasLogoutRequest("/SLOService.saml2", "GET"))
1247-
// add logout handlers
1248-
);
1229+
.addFilterBefore(filter, CsrfFilter.class);
12491230
----
12501231

12511232
=== Customizing `<saml2:LogoutRequest>` Resolution

saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,9 +1004,9 @@ public static final class Builder {
10041004

10051005
private Saml2MessageBinding assertionConsumerServiceBinding = Saml2MessageBinding.POST;
10061006

1007-
private String singleLogoutServiceLocation = "{baseUrl}/logout";
1007+
private String singleLogoutServiceLocation = "{baseUrl}/logout/saml2";
10081008

1009-
private String singleLogoutServiceResponseLocation = "{baseUrl}/logout";
1009+
private String singleLogoutServiceResponseLocation = "{baseUrl}/logout/saml2";
10101010

10111011
private Saml2MessageBinding singleLogoutServiceBinding = Saml2MessageBinding.POST;
10121012

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2002-2021 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.saml2.provider.service.web.authentication.logout;
18+
19+
import java.io.IOException;
20+
21+
import javax.servlet.FilterChain;
22+
import javax.servlet.ServletException;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
25+
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.context.SecurityContextHolder;
28+
import org.springframework.security.web.authentication.logout.CompositeLogoutHandler;
29+
import org.springframework.security.web.authentication.logout.LogoutHandler;
30+
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
31+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
32+
import org.springframework.security.web.util.matcher.RequestMatcher;
33+
import org.springframework.util.Assert;
34+
import org.springframework.web.filter.OncePerRequestFilter;
35+
36+
/**
37+
* A filter for handling logout requests in the form of a &lt;saml2:LogoutRequest&gt; or a
38+
* &lt;saml2:LogoutResponse&gt;.
39+
*
40+
* @author Josh Cummings
41+
* @since 5.5
42+
*/
43+
public class Saml2LogoutFilter extends OncePerRequestFilter {
44+
45+
private static final String DEFAULT_LOGOUT_ENDPOINT = "/logout/saml2";
46+
47+
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher(DEFAULT_LOGOUT_ENDPOINT);
48+
49+
private final LogoutHandler logoutHandler;
50+
51+
private final LogoutSuccessHandler logoutSuccessHandler;
52+
53+
public Saml2LogoutFilter(LogoutSuccessHandler logoutSuccessHandler, LogoutHandler... logoutHandler) {
54+
this.logoutSuccessHandler = logoutSuccessHandler;
55+
this.logoutHandler = new CompositeLogoutHandler(logoutHandler);
56+
}
57+
58+
@Override
59+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
60+
throws ServletException, IOException {
61+
62+
if (!this.logoutRequestMatcher.matches(request)) {
63+
chain.doFilter(request, response);
64+
return;
65+
}
66+
67+
if (!hasSamlRequestOrResponse(request)) {
68+
chain.doFilter(request, response);
69+
return;
70+
}
71+
72+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
73+
if (authentication == null) {
74+
chain.doFilter(request, response);
75+
return;
76+
}
77+
78+
this.logoutHandler.logout(request, response, authentication);
79+
this.logoutSuccessHandler.onLogoutSuccess(request, response, authentication);
80+
}
81+
82+
public void setLogoutRequestMatcher(RequestMatcher logoutRequestMatcher) {
83+
Assert.notNull(logoutRequestMatcher, "logoutRequestMatcher cannot be null");
84+
this.logoutRequestMatcher = logoutRequestMatcher;
85+
}
86+
87+
private boolean hasSamlRequestOrResponse(HttpServletRequest request) {
88+
return request.getParameter("SAMLRequest") != null || request.getParameter("SAMLResponse") != null;
89+
}
90+
91+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
* @author Josh Cummings
5151
* @since 5.5
5252
*/
53-
public final class Saml2RelyingPartyInitiatedLogoutFilter extends OncePerRequestFilter {
53+
public final class Saml2LogoutRequestFilter extends OncePerRequestFilter {
5454

5555
private RequestMatcher logoutRequestMatcher = new AntPathRequestMatcher("/saml2/logout", "POST");
5656

@@ -59,11 +59,10 @@ public final class Saml2RelyingPartyInitiatedLogoutFilter extends OncePerRequest
5959
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
6060

6161
/**
62-
* Constructs a {@link Saml2RelyingPartyInitiatedLogoutFilter} with the provided
63-
* parameters
62+
* Constructs a {@link Saml2LogoutRequestFilter} with the provided parameters
6463
* @param logoutRequestResolver the {@link Saml2LogoutRequestResolver} to use
6564
*/
66-
public Saml2RelyingPartyInitiatedLogoutFilter(Saml2LogoutRequestResolver logoutRequestResolver) {
65+
public Saml2LogoutRequestFilter(Saml2LogoutRequestResolver logoutRequestResolver) {
6766
Assert.notNull(logoutRequestResolver, "logoutRequestResolver cannot be null");
6867
this.logoutRequestResolver = logoutRequestResolver;
6968
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,18 @@
4343
* @author Josh Cummings
4444
* @since 5.5
4545
*/
46-
public final class Saml2AssertingPartyInitiatedLogoutSuccessHandler implements LogoutSuccessHandler {
46+
public final class Saml2ResponseLogoutSuccessHandler implements LogoutSuccessHandler {
4747

4848
private final Saml2LogoutResponseResolver logoutResponseResolver;
4949

5050
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
5151

5252
/**
53-
* Constructs a {@link Saml2AssertingPartyInitiatedLogoutSuccessHandler} using the
54-
* provided parameters
53+
* Constructs a {@link Saml2ResponseLogoutSuccessHandler} using the provided
54+
* parameters
5555
* @param logoutResponseResolver the {@link Saml2LogoutResponseResolver} to use
5656
*/
57-
public Saml2AssertingPartyInitiatedLogoutSuccessHandler(Saml2LogoutResponseResolver logoutResponseResolver) {
57+
public Saml2ResponseLogoutSuccessHandler(Saml2LogoutResponseResolver logoutResponseResolver) {
5858
this.logoutResponseResolver = logoutResponseResolver;
5959
}
6060

saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/provider/service/web/DefaultRelyingPartyRegistrationResolverTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public void resolveWhenRequestContainsRegistrationIdThenResolves() {
5252
.isEqualTo("http://localhost/saml2/service-provider-metadata/" + this.registration.getRegistrationId());
5353
assertThat(registration.getAssertionConsumerServiceLocation())
5454
.isEqualTo("http://localhost/login/saml2/sso/" + this.registration.getRegistrationId());
55-
assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo("http://localhost/logout");
56-
assertThat(registration.getSingleLogoutServiceResponseLocation()).isEqualTo("http://localhost/logout");
55+
assertThat(registration.getSingleLogoutServiceLocation()).isEqualTo("http://localhost/logout/saml2");
56+
assertThat(registration.getSingleLogoutServiceResponseLocation()).isEqualTo("http://localhost/logout/saml2");
5757
}
5858

5959
@Test

0 commit comments

Comments
 (0)