Skip to content

Commit 98e3fe8

Browse files
committed
Add OpenID Connect 1.0 Logout Endpoint
Closes gh-266
1 parent 7c6516b commit 98e3fe8

File tree

66 files changed

+2718
-84
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2718
-84
lines changed

docs/src/docs/asciidoc/examples/src/main/java/sample/jpa/entity/client/Client.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -39,6 +39,8 @@ public class Client {
3939
@Column(length = 1000)
4040
private String redirectUris;
4141
@Column(length = 1000)
42+
private String postLogoutRedirectUris;
43+
@Column(length = 1000)
4244
private String scopes;
4345
@Column(length = 2000)
4446
private String clientSettings;
@@ -118,6 +120,14 @@ public void setRedirectUris(String redirectUris) {
118120
this.redirectUris = redirectUris;
119121
}
120122

123+
public String getPostLogoutRedirectUris() {
124+
return this.postLogoutRedirectUris;
125+
}
126+
127+
public void setPostLogoutRedirectUris(String postLogoutRedirectUris) {
128+
this.postLogoutRedirectUris = postLogoutRedirectUris;
129+
}
130+
121131
public String getScopes() {
122132
return scopes;
123133
}

docs/src/docs/asciidoc/examples/src/main/java/sample/jpa/repository/authorization/AuthorizationRepository.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 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.
@@ -30,10 +30,12 @@ public interface AuthorizationRepository extends JpaRepository<Authorization, St
3030
Optional<Authorization> findByAuthorizationCodeValue(String authorizationCode);
3131
Optional<Authorization> findByAccessTokenValue(String accessToken);
3232
Optional<Authorization> findByRefreshTokenValue(String refreshToken);
33+
Optional<Authorization> findByOidcIdTokenValue(String idToken);
3334
@Query("select a from Authorization a where a.state = :token" +
3435
" or a.authorizationCodeValue = :token" +
3536
" or a.accessTokenValue = :token" +
36-
" or a.refreshTokenValue = :token"
37+
" or a.refreshTokenValue = :token" +
38+
" or a.oidcIdTokenValue = :token"
3739
)
38-
Optional<Authorization> findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(@Param("token") String token);
40+
Optional<Authorization> findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValueOrOidcIdTokenValue(@Param("token") String token);
3941
}

docs/src/docs/asciidoc/examples/src/main/java/sample/jpa/service/authorization/JpaOAuth2AuthorizationService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 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.
@@ -35,6 +35,7 @@
3535
import org.springframework.security.oauth2.core.OAuth2Token;
3636
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
3737
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
38+
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
3839
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
3940
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
4041
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -88,7 +89,7 @@ public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType)
8889

8990
Optional<Authorization> result;
9091
if (tokenType == null) {
91-
result = this.authorizationRepository.findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValue(token);
92+
result = this.authorizationRepository.findByStateOrAuthorizationCodeValueOrAccessTokenValueOrRefreshTokenValueOrOidcIdTokenValue(token);
9293
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
9394
result = this.authorizationRepository.findByState(token);
9495
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
@@ -97,6 +98,8 @@ public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType)
9798
result = this.authorizationRepository.findByAccessTokenValue(token);
9899
} else if (OAuth2ParameterNames.REFRESH_TOKEN.equals(tokenType.getValue())) {
99100
result = this.authorizationRepository.findByRefreshTokenValue(token);
101+
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
102+
result = this.authorizationRepository.findByOidcIdTokenValue(token);
100103
} else {
101104
result = Optional.empty();
102105
}

docs/src/docs/asciidoc/examples/src/main/java/sample/jpa/service/client/JpaRegisteredClientRepository.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2023 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.
@@ -78,6 +78,8 @@ private RegisteredClient toObject(Client client) {
7878
client.getAuthorizationGrantTypes());
7979
Set<String> redirectUris = StringUtils.commaDelimitedListToSet(
8080
client.getRedirectUris());
81+
Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(
82+
client.getPostLogoutRedirectUris());
8183
Set<String> clientScopes = StringUtils.commaDelimitedListToSet(
8284
client.getScopes());
8385

@@ -94,6 +96,7 @@ private RegisteredClient toObject(Client client) {
9496
authorizationGrantTypes.forEach(grantType ->
9597
grantTypes.add(resolveAuthorizationGrantType(grantType))))
9698
.redirectUris((uris) -> uris.addAll(redirectUris))
99+
.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
97100
.scopes((scopes) -> scopes.addAll(clientScopes));
98101

99102
Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
@@ -124,6 +127,7 @@ private Client toEntity(RegisteredClient registeredClient) {
124127
entity.setClientAuthenticationMethods(StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods));
125128
entity.setAuthorizationGrantTypes(StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes));
126129
entity.setRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
130+
entity.setPostLogoutRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris()));
127131
entity.setScopes(StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes()));
128132
entity.setClientSettings(writeMap(registeredClient.getClientSettings().getSettings()));
129133
entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));

docs/src/docs/asciidoc/examples/src/test/java/sample/gettingStarted/SecurityConfigTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -102,7 +102,8 @@ public void oidcLoginWhenGettingStartedConfigUsedThenSuccess() throws Exception
102102
assertThatAuthorization(refreshToken, null).isNotNull();
103103

104104
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
105-
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
105+
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
106+
assertThatAuthorization(idToken, null).isNotNull();
106107

107108
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
108109
assertThat(authorization.getToken(idToken)).isNotNull();

docs/src/docs/asciidoc/examples/src/test/java/sample/jpa/JpaTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -117,7 +117,8 @@ public void oidcLoginWhenJpaCoreServicesAutowiredThenUsed() throws Exception {
117117
assertThatAuthorization(refreshToken, null).isNotNull();
118118

119119
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
120-
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
120+
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
121+
assertThatAuthorization(idToken, null).isNotNull();
121122

122123
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
123124
assertThat(authorization.getToken(idToken)).isNotNull();

docs/src/docs/asciidoc/examples/src/test/java/sample/util/RegisteredClients.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -37,6 +37,7 @@ public static RegisteredClient messagingClient() {
3737
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
3838
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
3939
.redirectUri("http://127.0.0.1:8080/authorized")
40+
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
4041
.scope(OidcScopes.OPENID)
4142
.scope("message.read")
4243
.scope("message.write")

docs/src/docs/asciidoc/guides/how-to-jpa.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ CREATE TABLE client (
4545
clientAuthenticationMethods varchar(1000) NOT NULL,
4646
authorizationGrantTypes varchar(1000) NOT NULL,
4747
redirectUris varchar(1000) DEFAULT NULL,
48+
postLogoutRedirectUris varchar(1000) DEFAULT NULL,
4849
scopes varchar(1000) NOT NULL,
4950
clientSettings varchar(2000) NOT NULL,
5051
tokenSettings varchar(2000) NOT NULL,

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/InMemoryOAuth2AuthorizationService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -26,6 +26,8 @@
2626
import org.springframework.security.oauth2.core.OAuth2AccessToken;
2727
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
2828
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
29+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
30+
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
2931
import org.springframework.util.Assert;
3032

3133
/**
@@ -150,13 +152,16 @@ private static boolean hasToken(OAuth2Authorization authorization, String token,
150152
return matchesState(authorization, token) ||
151153
matchesAuthorizationCode(authorization, token) ||
152154
matchesAccessToken(authorization, token) ||
155+
matchesIdToken(authorization, token) ||
153156
matchesRefreshToken(authorization, token);
154157
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
155158
return matchesState(authorization, token);
156159
} else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
157160
return matchesAuthorizationCode(authorization, token);
158161
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
159162
return matchesAccessToken(authorization, token);
163+
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
164+
return matchesIdToken(authorization, token);
160165
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
161166
return matchesRefreshToken(authorization, token);
162167
}
@@ -185,6 +190,12 @@ private static boolean matchesRefreshToken(OAuth2Authorization authorization, St
185190
return refreshToken != null && refreshToken.getToken().getTokenValue().equals(token);
186191
}
187192

193+
private static boolean matchesIdToken(OAuth2Authorization authorization, String token) {
194+
OAuth2Authorization.Token<OidcIdToken> idToken =
195+
authorization.getToken(OidcIdToken.class);
196+
return idToken != null && idToken.getToken().getTokenValue().equals(token);
197+
}
198+
188199
private static final class MaxSizeHashMap<K, V> extends LinkedHashMap<K, V> {
189200
private final int maxSize;
190201

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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.
@@ -53,6 +53,7 @@
5353
import org.springframework.security.oauth2.core.OAuth2Token;
5454
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
5555
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
56+
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
5657
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
5758
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
5859
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
@@ -112,11 +113,12 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic
112113

113114
private static final String PK_FILTER = "id = ?";
114115
private static final String UNKNOWN_TOKEN_TYPE_FILTER = "state = ? OR authorization_code_value = ? OR " +
115-
"access_token_value = ? OR refresh_token_value = ?";
116+
"access_token_value = ? OR oidc_id_token_value = ? OR refresh_token_value = ?";
116117

117118
private static final String STATE_FILTER = "state = ?";
118119
private static final String AUTHORIZATION_CODE_FILTER = "authorization_code_value = ?";
119120
private static final String ACCESS_TOKEN_FILTER = "access_token_value = ?";
121+
private static final String ID_TOKEN_FILTER = "oidc_id_token_value = ?";
120122
private static final String REFRESH_TOKEN_FILTER = "refresh_token_value = ?";
121123

122124
// @formatter:off
@@ -240,6 +242,7 @@ public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType t
240242
parameters.add(new SqlParameterValue(Types.VARCHAR, token));
241243
parameters.add(mapToSqlParameter("authorization_code_value", token));
242244
parameters.add(mapToSqlParameter("access_token_value", token));
245+
parameters.add(mapToSqlParameter("oidc_id_token_value", token));
243246
parameters.add(mapToSqlParameter("refresh_token_value", token));
244247
return findBy(UNKNOWN_TOKEN_TYPE_FILTER, parameters);
245248
} else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
@@ -251,6 +254,9 @@ public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType t
251254
} else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
252255
parameters.add(mapToSqlParameter("access_token_value", token));
253256
return findBy(ACCESS_TOKEN_FILTER, parameters);
257+
} else if (OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) {
258+
parameters.add(mapToSqlParameter("oidc_id_token_value", token));
259+
return findBy(ID_TOKEN_FILTER, parameters);
254260
} else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
255261
parameters.add(mapToSqlParameter("refresh_token_value", token));
256262
return findBy(REFRESH_TOKEN_FILTER, parameters);

oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2022 the original author or authors.
2+
* Copyright 2020-2023 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,8 +16,11 @@
1616
package org.springframework.security.oauth2.server.authorization.authentication;
1717

1818
import java.security.Principal;
19+
import java.util.ArrayList;
1920
import java.util.Collections;
21+
import java.util.Comparator;
2022
import java.util.HashMap;
23+
import java.util.List;
2124
import java.util.Map;
2225

2326
import org.apache.commons.logging.Log;
@@ -27,6 +30,8 @@
2730
import org.springframework.security.authentication.AuthenticationProvider;
2831
import org.springframework.security.core.Authentication;
2932
import org.springframework.security.core.AuthenticationException;
33+
import org.springframework.security.core.session.SessionInformation;
34+
import org.springframework.security.core.session.SessionRegistry;
3035
import org.springframework.security.oauth2.core.AuthorizationGrantType;
3136
import org.springframework.security.oauth2.core.ClaimAccessor;
3237
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -52,6 +57,7 @@
5257
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
5358
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
5459
import org.springframework.util.Assert;
60+
import org.springframework.util.CollectionUtils;
5561
import org.springframework.util.StringUtils;
5662

5763
import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
@@ -79,6 +85,7 @@ public final class OAuth2AuthorizationCodeAuthenticationProvider implements Auth
7985
private final Log logger = LogFactory.getLog(getClass());
8086
private final OAuth2AuthorizationService authorizationService;
8187
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
88+
private SessionRegistry sessionRegistry;
8289

8390
/**
8491
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
@@ -149,10 +156,12 @@ public Authentication authenticate(Authentication authentication) throws Authent
149156
this.logger.trace("Validated token request parameters");
150157
}
151158

159+
Authentication principal = authorization.getAttribute(Principal.class.getName());
160+
152161
// @formatter:off
153162
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
154163
.registeredClient(registeredClient)
155-
.principal(authorization.getAttribute(Principal.class.getName()))
164+
.principal(principal)
156165
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
157166
.authorization(authorization)
158167
.authorizedScopes(authorization.getAuthorizedScopes())
@@ -210,6 +219,10 @@ public Authentication authenticate(Authentication authentication) throws Authent
210219
// ----- ID token -----
211220
OidcIdToken idToken;
212221
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
222+
SessionInformation sessionInformation = getSessionInformation(principal);
223+
if (sessionInformation != null) {
224+
tokenContextBuilder.put(SessionInformation.class, sessionInformation);
225+
}
213226
// @formatter:off
214227
tokenContext = tokenContextBuilder
215228
.tokenType(ID_TOKEN_TOKEN_TYPE)
@@ -265,4 +278,32 @@ public boolean supports(Class<?> authentication) {
265278
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
266279
}
267280

281+
/**
282+
* Sets the {@link SessionRegistry} used to track OpenID Connect sessions.
283+
*
284+
* @param sessionRegistry the {@link SessionRegistry} used to track OpenID Connect sessions
285+
* @since 1.1.0
286+
*/
287+
public void setSessionRegistry(SessionRegistry sessionRegistry) {
288+
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
289+
this.sessionRegistry = sessionRegistry;
290+
}
291+
292+
private SessionInformation getSessionInformation(Authentication principal) {
293+
SessionInformation sessionInformation = null;
294+
if (this.sessionRegistry != null) {
295+
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(principal.getPrincipal(), false);
296+
if (!CollectionUtils.isEmpty(sessions)) {
297+
sessionInformation = sessions.get(0);
298+
if (sessions.size() > 1) {
299+
// Get the most recent session
300+
sessions = new ArrayList<>(sessions);
301+
sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
302+
sessionInformation = sessions.get(sessions.size() - 1);
303+
}
304+
}
305+
}
306+
return sessionInformation;
307+
}
308+
268309
}

0 commit comments

Comments
 (0)