Skip to content

Commit f885df4

Browse files
committed
Allow customizing LogoutHandler in OidcLogoutEndpointFilter
Closes gh-1244
1 parent 19dfcd4 commit f885df4

File tree

4 files changed

+223
-57
lines changed

4 files changed

+223
-57
lines changed

docs/modules/ROOT/pages/protocol-endpoints.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity h
545545

546546
* `*AuthenticationConverter*` -- An `OidcLogoutAuthenticationConverter`.
547547
* `*AuthenticationManager*` -- An `AuthenticationManager` composed of `OidcLogoutAuthenticationProvider`.
548-
* `*AuthenticationSuccessHandler*` -- An internal implementation that handles an "`authenticated`" `OidcLogoutAuthenticationToken` and performs the logout.
548+
* `*AuthenticationSuccessHandler*` -- An `OidcLogoutAuthenticationSuccessHandler`.
549549
* `*AuthenticationFailureHandler*` -- An internal implementation that uses the `OAuth2Error` associated with the `OAuth2AuthenticationException` and returns the `OAuth2Error` response.
550550

551551
[NOTE]

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/oidc/web/OidcLogoutEndpointFilter.java

+4-56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
1616
package org.springframework.security.oauth2.server.authorization.oidc.web;
1717

1818
import java.io.IOException;
19-
import java.nio.charset.StandardCharsets;
2019

2120
import jakarta.servlet.FilterChain;
2221
import jakarta.servlet.ServletException;
@@ -32,34 +31,26 @@
3231
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
3332
import org.springframework.security.oauth2.core.OAuth2Error;
3433
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
35-
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
3634
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider;
3735
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
3836
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter;
39-
import org.springframework.security.web.DefaultRedirectStrategy;
40-
import org.springframework.security.web.RedirectStrategy;
37+
import org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationSuccessHandler;
4138
import org.springframework.security.web.authentication.AuthenticationConverter;
4239
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
4340
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
44-
import org.springframework.security.web.authentication.logout.LogoutHandler;
45-
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
46-
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
47-
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
4841
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
4942
import org.springframework.security.web.util.matcher.OrRequestMatcher;
5043
import org.springframework.security.web.util.matcher.RequestMatcher;
5144
import org.springframework.util.Assert;
52-
import org.springframework.util.StringUtils;
5345
import org.springframework.web.filter.OncePerRequestFilter;
54-
import org.springframework.web.util.UriComponentsBuilder;
55-
import org.springframework.web.util.UriUtils;
5646

5747
/**
5848
* A {@code Filter} that processes OpenID Connect 1.0 RP-Initiated Logout Requests.
5949
*
6050
* @author Joe Grandja
6151
* @since 1.1
6252
* @see OidcLogoutAuthenticationConverter
53+
* @see OidcLogoutAuthenticationSuccessHandler
6354
* @see OidcLogoutAuthenticationProvider
6455
* @see <a href="https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout">2.
6556
* RP-Initiated Logout</a>
@@ -76,15 +67,9 @@ public final class OidcLogoutEndpointFilter extends OncePerRequestFilter {
7667

7768
private final RequestMatcher logoutEndpointMatcher;
7869

79-
private final LogoutHandler logoutHandler;
80-
81-
private final LogoutSuccessHandler logoutSuccessHandler;
82-
83-
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
84-
8570
private AuthenticationConverter authenticationConverter;
8671

87-
private AuthenticationSuccessHandler authenticationSuccessHandler = this::performLogout;
72+
private AuthenticationSuccessHandler authenticationSuccessHandler = new OidcLogoutAuthenticationSuccessHandler();
8873

8974
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
9075

@@ -109,10 +94,6 @@ public OidcLogoutEndpointFilter(AuthenticationManager authenticationManager, Str
10994
this.logoutEndpointMatcher = new OrRequestMatcher(
11095
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.GET.name()),
11196
new AntPathRequestMatcher(logoutEndpointUri, HttpMethod.POST.name()));
112-
this.logoutHandler = new SecurityContextLogoutHandler();
113-
SimpleUrlLogoutSuccessHandler urlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
114-
urlLogoutSuccessHandler.setDefaultTargetUrl("/");
115-
this.logoutSuccessHandler = urlLogoutSuccessHandler;
11697
this.authenticationConverter = new OidcLogoutAuthenticationConverter();
11798
}
11899

@@ -187,39 +168,6 @@ public void setAuthenticationFailureHandler(AuthenticationFailureHandler authent
187168
this.authenticationFailureHandler = authenticationFailureHandler;
188169
}
189170

190-
private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
191-
throws IOException, ServletException {
192-
193-
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication;
194-
195-
// Check for active user session
196-
if (oidcLogoutAuthentication.isPrincipalAuthenticated()
197-
&& StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) {
198-
// Perform logout
199-
this.logoutHandler.logout(request, response, (Authentication) oidcLogoutAuthentication.getPrincipal());
200-
}
201-
202-
if (oidcLogoutAuthentication.isAuthenticated()
203-
&& StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
204-
// Perform post-logout redirect
205-
UriComponentsBuilder uriBuilder = UriComponentsBuilder
206-
.fromUriString(oidcLogoutAuthentication.getPostLogoutRedirectUri());
207-
String redirectUri;
208-
if (StringUtils.hasText(oidcLogoutAuthentication.getState())) {
209-
uriBuilder.queryParam(OAuth2ParameterNames.STATE,
210-
UriUtils.encode(oidcLogoutAuthentication.getState(), StandardCharsets.UTF_8));
211-
}
212-
// build(true) -> Components are explicitly encoded
213-
redirectUri = uriBuilder.build(true).toUriString();
214-
this.redirectStrategy.sendRedirect(request, response, redirectUri);
215-
}
216-
else {
217-
// Perform default redirect
218-
this.logoutSuccessHandler.onLogoutSuccess(request, response,
219-
(Authentication) oidcLogoutAuthentication.getPrincipal());
220-
}
221-
}
222-
223171
private void sendErrorResponse(HttpServletRequest request, HttpServletResponse response,
224172
AuthenticationException exception) throws IOException {
225173

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2020-2024 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+
package org.springframework.security.oauth2.server.authorization.oidc.web.authentication;
17+
18+
import java.io.IOException;
19+
import java.nio.charset.StandardCharsets;
20+
21+
import jakarta.servlet.ServletException;
22+
import jakarta.servlet.http.HttpServletRequest;
23+
import jakarta.servlet.http.HttpServletResponse;
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.security.core.Authentication;
28+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
29+
import org.springframework.security.oauth2.core.OAuth2Error;
30+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
31+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
32+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
33+
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter;
34+
import org.springframework.security.web.DefaultRedirectStrategy;
35+
import org.springframework.security.web.RedirectStrategy;
36+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
37+
import org.springframework.security.web.authentication.logout.LogoutHandler;
38+
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
39+
import org.springframework.util.Assert;
40+
import org.springframework.util.StringUtils;
41+
import org.springframework.web.util.UriComponentsBuilder;
42+
import org.springframework.web.util.UriUtils;
43+
44+
/**
45+
* An implementation of an {@link AuthenticationSuccessHandler} used for handling an
46+
* {@link OidcLogoutAuthenticationToken} and performing the OpenID Connect 1.0
47+
* RP-Initiated Logout.
48+
*
49+
* @author Joe Grandja
50+
* @since 1.4
51+
* @see OidcLogoutEndpointFilter#setAuthenticationSuccessHandler(AuthenticationSuccessHandler)
52+
* @see LogoutHandler
53+
*/
54+
public final class OidcLogoutAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
55+
56+
private final Log logger = LogFactory.getLog(getClass());
57+
58+
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
59+
60+
private final SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler();
61+
62+
private LogoutHandler logoutHandler = this::performLogout;
63+
64+
@Override
65+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
66+
Authentication authentication) throws IOException, ServletException {
67+
68+
if (!(authentication instanceof OidcLogoutAuthenticationToken)) {
69+
if (this.logger.isErrorEnabled()) {
70+
this.logger.error(Authentication.class.getSimpleName() + " must be of type "
71+
+ OidcLogoutAuthenticationToken.class.getName() + " but was "
72+
+ authentication.getClass().getName());
73+
}
74+
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
75+
"Unable to process the OpenID Connect 1.0 RP-Initiated Logout response.", null);
76+
throw new OAuth2AuthenticationException(error);
77+
}
78+
79+
this.logoutHandler.logout(request, response, authentication);
80+
81+
sendLogoutRedirect(request, response, authentication);
82+
}
83+
84+
/**
85+
* Sets the {@link LogoutHandler} used for performing logout.
86+
* @param logoutHandler the {@link LogoutHandler} used for performing logout
87+
*/
88+
public void setLogoutHandler(LogoutHandler logoutHandler) {
89+
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
90+
this.logoutHandler = logoutHandler;
91+
}
92+
93+
private void performLogout(HttpServletRequest request, HttpServletResponse response,
94+
Authentication authentication) {
95+
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication;
96+
97+
// Check for active user session
98+
if (oidcLogoutAuthentication.isPrincipalAuthenticated()) {
99+
this.securityContextLogoutHandler.logout(request, response,
100+
(Authentication) oidcLogoutAuthentication.getPrincipal());
101+
}
102+
}
103+
104+
private void sendLogoutRedirect(HttpServletRequest request, HttpServletResponse response,
105+
Authentication authentication) throws IOException {
106+
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication;
107+
108+
String redirectUri = "/";
109+
if (oidcLogoutAuthentication.isAuthenticated()
110+
&& StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
111+
// Use the `post_logout_redirect_uri` parameter
112+
UriComponentsBuilder uriBuilder = UriComponentsBuilder
113+
.fromUriString(oidcLogoutAuthentication.getPostLogoutRedirectUri());
114+
if (StringUtils.hasText(oidcLogoutAuthentication.getState())) {
115+
uriBuilder.queryParam(OAuth2ParameterNames.STATE,
116+
UriUtils.encode(oidcLogoutAuthentication.getState(), StandardCharsets.UTF_8));
117+
}
118+
// build(true) -> Components are explicitly encoded
119+
redirectUri = uriBuilder.build(true).toUriString();
120+
}
121+
this.redirectStrategy.sendRedirect(request, response, redirectUri);
122+
}
123+
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2020-2024 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+
package org.springframework.security.oauth2.server.authorization.oidc.web.authentication;
17+
18+
import jakarta.servlet.http.HttpServletRequest;
19+
import jakarta.servlet.http.HttpServletResponse;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.mock.web.MockHttpServletRequest;
24+
import org.springframework.mock.web.MockHttpServletResponse;
25+
import org.springframework.mock.web.MockHttpSession;
26+
import org.springframework.security.authentication.TestingAuthenticationToken;
27+
import org.springframework.security.core.Authentication;
28+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
29+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
30+
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken;
31+
import org.springframework.security.web.authentication.logout.LogoutHandler;
32+
33+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
34+
import static org.mockito.ArgumentMatchers.any;
35+
import static org.mockito.Mockito.mock;
36+
import static org.mockito.Mockito.verify;
37+
38+
/**
39+
* Tests for {@link OidcLogoutAuthenticationSuccessHandler}.
40+
*
41+
* @author Joe Grandja
42+
*/
43+
public class OidcLogoutAuthenticationSuccessHandlerTests {
44+
45+
private TestingAuthenticationToken principal;
46+
47+
private final OidcLogoutAuthenticationSuccessHandler authenticationSuccessHandler = new OidcLogoutAuthenticationSuccessHandler();
48+
49+
@BeforeEach
50+
public void setUp() {
51+
this.principal = new TestingAuthenticationToken("principal", "credentials");
52+
this.principal.setAuthenticated(true);
53+
}
54+
55+
@Test
56+
public void setLogoutHandlerWhenNullThenThrowIllegalArgumentException() {
57+
// @formatter:off
58+
assertThatThrownBy(() -> this.authenticationSuccessHandler.setLogoutHandler(null))
59+
.isInstanceOf(IllegalArgumentException.class)
60+
.hasMessage("logoutHandler cannot be null");
61+
// @formatter:on
62+
}
63+
64+
@Test
65+
public void onAuthenticationSuccessWhenInvalidAuthenticationTypeThenThrowOAuth2AuthenticationException() {
66+
MockHttpServletRequest request = new MockHttpServletRequest();
67+
MockHttpServletResponse response = new MockHttpServletResponse();
68+
69+
assertThatThrownBy(
70+
() -> this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, this.principal))
71+
.isInstanceOf(OAuth2AuthenticationException.class)
72+
.extracting((ex) -> ((OAuth2AuthenticationException) ex).getError())
73+
.extracting("errorCode")
74+
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR);
75+
}
76+
77+
@Test
78+
public void onAuthenticationSuccessWhenLogoutHandlerSetThenUsed() throws Exception {
79+
LogoutHandler logoutHandler = mock(LogoutHandler.class);
80+
this.authenticationSuccessHandler.setLogoutHandler(logoutHandler);
81+
82+
MockHttpServletRequest request = new MockHttpServletRequest();
83+
MockHttpSession session = (MockHttpSession) request.getSession(true);
84+
MockHttpServletResponse response = new MockHttpServletResponse();
85+
86+
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken("id-token", this.principal,
87+
session.getId(), null, null, null);
88+
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authentication);
89+
90+
verify(logoutHandler).logout(any(HttpServletRequest.class), any(HttpServletResponse.class),
91+
any(Authentication.class));
92+
}
93+
94+
}

0 commit comments

Comments
 (0)