Skip to content

Commit 080d8bb

Browse files
committed
Expose OidcBackChannelLogoutHandler
This component already uses by default a URI that doesn't require a CSRF token and aalready allows for configuring a cookie name. So, by making it public and configurable in the DSL, both of these tickets quite naturally close. Closes gh-13841 Closes gh-14904
1 parent 5f1028d commit 080d8bb

File tree

22 files changed

+910
-137
lines changed

22 files changed

+910
-137
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthentication.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.springframework.security.authentication.AbstractAuthenticationToken;
2222
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
23+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2324

2425
/**
2526
* An {@link org.springframework.security.core.Authentication} implementation that
@@ -37,13 +38,16 @@ class OidcBackChannelLogoutAuthentication extends AbstractAuthenticationToken {
3738

3839
private final OidcLogoutToken logoutToken;
3940

41+
private final ClientRegistration clientRegistration;
42+
4043
/**
4144
* Construct an {@link OidcBackChannelLogoutAuthentication}
4245
* @param logoutToken a deserialized, verified OIDC Logout Token
4346
*/
44-
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken) {
47+
OidcBackChannelLogoutAuthentication(OidcLogoutToken logoutToken, ClientRegistration clientRegistration) {
4548
super(Collections.emptyList());
4649
this.logoutToken = logoutToken;
50+
this.clientRegistration = clientRegistration;
4751
setAuthenticated(true);
4852
}
4953

@@ -63,4 +67,8 @@ public OidcLogoutToken getCredentials() {
6367
return this.logoutToken;
6468
}
6569

70+
ClientRegistration getClientRegistration() {
71+
return this.clientRegistration;
72+
}
73+
6674
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutAuthenticationProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
9999
OidcLogoutToken oidcLogoutToken = OidcLogoutToken.withTokenValue(logoutToken)
100100
.claims((claims) -> claims.putAll(jwt.getClaims()))
101101
.build();
102-
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken);
102+
return new OidcBackChannelLogoutAuthentication(oidcLogoutToken, registration);
103103
}
104104

105105
/**

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutFilter.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
5858

5959
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
6060

61-
private LogoutHandler logoutHandler = new OidcBackChannelLogoutHandler();
61+
private final LogoutHandler logoutHandler;
6262

6363
/**
6464
* Construct an {@link OidcBackChannelLogoutFilter}
@@ -68,11 +68,13 @@ class OidcBackChannelLogoutFilter extends OncePerRequestFilter {
6868
* Logout Tokens
6969
*/
7070
OidcBackChannelLogoutFilter(AuthenticationConverter authenticationConverter,
71-
AuthenticationManager authenticationManager) {
71+
AuthenticationManager authenticationManager, LogoutHandler logoutHandler) {
7272
Assert.notNull(authenticationConverter, "authenticationConverter cannot be null");
7373
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
74+
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
7475
this.authenticationConverter = authenticationConverter;
7576
this.authenticationManager = authenticationManager;
77+
this.logoutHandler = logoutHandler;
7678
}
7779

7880
/**
@@ -126,14 +128,4 @@ private OAuth2Error oauth2Error(Exception ex) {
126128
"https://openid.net/specs/openid-connect-backchannel-1_0.html#Validation");
127129
}
128130

129-
/**
130-
* The strategy for expiring all Client sessions indicated by the logout request.
131-
* Defaults to {@link OidcBackChannelLogoutHandler}.
132-
* @param logoutHandler the {@link LogoutHandler} to use
133-
*/
134-
void setLogoutHandler(LogoutHandler logoutHandler) {
135-
Assert.notNull(logoutHandler, "logoutHandler cannot be null");
136-
this.logoutHandler = logoutHandler;
137-
}
138-
139131
}

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcBackChannelLogoutHandler.java

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@
2929

3030
import org.springframework.http.HttpEntity;
3131
import org.springframework.http.HttpHeaders;
32+
import org.springframework.http.MediaType;
3233
import org.springframework.http.server.ServletServerHttpResponse;
3334
import org.springframework.security.core.Authentication;
34-
import org.springframework.security.oauth2.client.oidc.authentication.logout.OidcLogoutToken;
35-
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
3635
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
3736
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
3837
import org.springframework.security.oauth2.core.OAuth2Error;
3938
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
4039
import org.springframework.security.web.authentication.logout.LogoutHandler;
4140
import org.springframework.security.web.util.UrlUtils;
4241
import org.springframework.util.Assert;
42+
import org.springframework.util.LinkedMultiValueMap;
43+
import org.springframework.util.MultiValueMap;
4344
import org.springframework.web.client.RestClientException;
4445
import org.springframework.web.client.RestOperations;
4546
import org.springframework.web.client.RestTemplate;
@@ -51,25 +52,29 @@
5152
* Back-Channel Logout Token and invalidates each one.
5253
*
5354
* @author Josh Cummings
54-
* @since 6.2
55+
* @since 6.4
5556
* @see <a target="_blank" href=
5657
* "https://openid.net/specs/openid-connect-backchannel-1_0.html">OIDC Back-Channel Logout
5758
* Spec</a>
5859
*/
59-
final class OidcBackChannelLogoutHandler implements LogoutHandler {
60+
public final class OidcBackChannelLogoutHandler implements LogoutHandler {
6061

6162
private final Log logger = LogFactory.getLog(getClass());
6263

63-
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
64+
private final OidcSessionRegistry sessionRegistry;
6465

6566
private RestOperations restOperations = new RestTemplate();
6667

67-
private String logoutUri = "{baseScheme}://localhost{basePort}/logout";
68+
private String logoutUri = "{baseUrl}/logout/connect/back-channel/{registrationId}";
6869

6970
private String sessionCookieName = "JSESSIONID";
7071

7172
private final OAuth2ErrorHttpMessageConverter errorHttpMessageConverter = new OAuth2ErrorHttpMessageConverter();
7273

74+
public OidcBackChannelLogoutHandler(OidcSessionRegistry sessionRegistry) {
75+
this.sessionRegistry = sessionRegistry;
76+
}
77+
7378
@Override
7479
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
7580
if (!(authentication instanceof OidcBackChannelLogoutAuthentication token)) {
@@ -86,7 +91,7 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
8691
for (OidcSessionInformation session : sessions) {
8792
totalCount++;
8893
try {
89-
eachLogout(request, session);
94+
eachLogout(request, token, session);
9095
invalidatedCount++;
9196
}
9297
catch (RestClientException ex) {
@@ -103,18 +108,23 @@ public void logout(HttpServletRequest request, HttpServletResponse response, Aut
103108
}
104109
}
105110

106-
private void eachLogout(HttpServletRequest request, OidcSessionInformation session) {
111+
private void eachLogout(HttpServletRequest request, OidcBackChannelLogoutAuthentication token,
112+
OidcSessionInformation session) {
107113
HttpHeaders headers = new HttpHeaders();
108114
headers.add(HttpHeaders.COOKIE, this.sessionCookieName + "=" + session.getSessionId());
109115
for (Map.Entry<String, String> credential : session.getAuthorities().entrySet()) {
110116
headers.add(credential.getKey(), credential.getValue());
111117
}
112-
String logout = computeLogoutEndpoint(request);
113-
HttpEntity<?> entity = new HttpEntity<>(null, headers);
118+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
119+
String logout = computeLogoutEndpoint(request, token);
120+
MultiValueMap<String, String> body = new LinkedMultiValueMap();
121+
body.add("logout_token", token.getPrincipal().getTokenValue());
122+
body.add("_spring_security_internal_logout", "true");
123+
HttpEntity<?> entity = new HttpEntity<>(body, headers);
114124
this.restOperations.postForEntity(logout, entity, Object.class);
115125
}
116126

117-
String computeLogoutEndpoint(HttpServletRequest request) {
127+
String computeLogoutEndpoint(HttpServletRequest request, OidcBackChannelLogoutAuthentication token) {
118128
// @formatter:off
119129
UriComponents uriComponents = UriComponentsBuilder
120130
.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
@@ -137,6 +147,9 @@ String computeLogoutEndpoint(HttpServletRequest request) {
137147
int port = uriComponents.getPort();
138148
uriVariables.put("basePort", (port == -1) ? "" : ":" + port);
139149

150+
String registrationId = token.getClientRegistration().getRegistrationId();
151+
uriVariables.put("registrationId", registrationId);
152+
140153
return UriComponentsBuilder.fromUriString(this.logoutUri)
141154
.buildAndExpand(uriVariables)
142155
.toUriString();
@@ -158,34 +171,13 @@ private void handleLogoutFailure(HttpServletResponse response, OAuth2Error error
158171
}
159172
}
160173

161-
/**
162-
* Use this {@link OidcSessionRegistry} to identify sessions to invalidate. Note that
163-
* this class uses
164-
* {@link OidcSessionRegistry#removeSessionInformation(OidcLogoutToken)} to identify
165-
* sessions.
166-
* @param sessionRegistry the {@link OidcSessionRegistry} to use
167-
*/
168-
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
169-
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
170-
this.sessionRegistry = sessionRegistry;
171-
}
172-
173-
/**
174-
* Use this {@link RestOperations} to perform the per-session back-channel logout
175-
* @param restOperations the {@link RestOperations} to use
176-
*/
177-
void setRestOperations(RestOperations restOperations) {
178-
Assert.notNull(restOperations, "restOperations cannot be null");
179-
this.restOperations = restOperations;
180-
}
181-
182174
/**
183175
* Use this logout URI for performing per-session logout. Defaults to {@code /logout}
184176
* since that is the default URI for
185177
* {@link org.springframework.security.web.authentication.logout.LogoutFilter}.
186178
* @param logoutUri the URI to use
187179
*/
188-
void setLogoutUri(String logoutUri) {
180+
public void setLogoutUri(String logoutUri) {
189181
Assert.hasText(logoutUri, "logoutUri cannot be empty");
190182
this.logoutUri = logoutUri;
191183
}
@@ -197,7 +189,7 @@ void setLogoutUri(String logoutUri) {
197189
* Note that if you are using Spring Session, this likely needs to change to SESSION.
198190
* @param sessionCookieName the cookie name to use
199191
*/
200-
void setSessionCookieName(String sessionCookieName) {
192+
public void setSessionCookieName(String sessionCookieName) {
201193
Assert.hasText(sessionCookieName, "clientSessionCookieName cannot be empty");
202194
this.sessionCookieName = sessionCookieName;
203195
}

0 commit comments

Comments
 (0)