Skip to content

Commit c9d9848

Browse files
committed
Ensure ID Token is updated after refresh token
1 parent 9c51507 commit c9d9848

File tree

9 files changed

+229
-5
lines changed

9 files changed

+229
-5
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3535
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3636
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
37+
import org.springframework.context.ApplicationContext;
38+
import org.springframework.context.ApplicationContextAware;
39+
import org.springframework.context.ApplicationEventPublisher;
3740
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
3841
import org.springframework.context.annotation.Bean;
3942
import org.springframework.context.annotation.Configuration;
@@ -160,7 +163,7 @@ private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
160163
* @since 6.2.0
161164
*/
162165
static final class OAuth2AuthorizedClientManagerRegistrar
163-
implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
166+
implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
164167

165168
static final String BEAN_NAME = "authorizedClientManagerRegistrar";
166169

@@ -179,6 +182,8 @@ static final class OAuth2AuthorizedClientManagerRegistrar
179182

180183
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
181184

185+
private ApplicationEventPublisher eventPublisher;
186+
182187
private ListableBeanFactory beanFactory;
183188

184189
@Override
@@ -302,6 +307,10 @@ private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
302307
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
303308
}
304309

310+
if (this.eventPublisher != null) {
311+
authorizedClientProvider.setApplicationEventPublisher(this.eventPublisher);
312+
}
313+
305314
return authorizedClientProvider;
306315
}
307316

@@ -423,6 +432,11 @@ private <T> T getBeanOfType(ResolvableType resolvableType) {
423432
return objectProvider.getIfAvailable();
424433
}
425434

435+
@Override
436+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
437+
this.eventPublisher = applicationContext;
438+
}
439+
426440
}
427441

428442
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
5757
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
5858
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
59+
import org.springframework.security.oauth2.client.oidc.authentication.RefreshOidcIdTokenHandler;
5960
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
6061
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
6162
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
@@ -393,6 +394,10 @@ public void init(B http) throws Exception {
393394
oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
394395
}
395396
http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider));
397+
398+
RefreshOidcIdTokenHandler refreshOidcIdTokenHandler = new RefreshOidcIdTokenHandler(
399+
oidcAuthorizationCodeAuthenticationProvider);
400+
registerDelegateApplicationListener(refreshOidcIdTokenHandler);
396401
}
397402
else {
398403
http.authenticationProvider(new OidcAuthenticationRequestChecker());

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3535
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3636
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
37+
import org.springframework.context.ApplicationEventPublisher;
3738
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
3839
import org.springframework.core.ResolvableType;
3940
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
@@ -197,6 +198,12 @@ private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
197198
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
198199
}
199200

201+
ApplicationEventPublisher applicationEventPublisher = getBeanOfType(
202+
ResolvableType.forClass(ApplicationEventPublisher.class));
203+
if (applicationEventPublisher != null) {
204+
authorizedClientProvider.setApplicationEventPublisher(applicationEventPublisher);
205+
}
206+
200207
return authorizedClientProvider;
201208
}
202209

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Map;
2626
import java.util.function.Consumer;
2727

28+
import org.springframework.context.ApplicationEventPublisher;
2829
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
2930
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
3031
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
@@ -359,6 +360,8 @@ public final class RefreshTokenGrantBuilder implements Builder {
359360

360361
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
361362

363+
private ApplicationEventPublisher eventPublisher;
364+
362365
private Duration clockSkew;
363366

364367
private Clock clock;
@@ -379,6 +382,17 @@ public RefreshTokenGrantBuilder accessTokenResponseClient(
379382
return this;
380383
}
381384

385+
/**
386+
* Sets the {@link ApplicationEventPublisher} used when an access token is
387+
* refreshed.
388+
* @param eventPublisher the {@link ApplicationEventPublisher}
389+
* @return the {@link RefreshTokenGrantBuilder}
390+
*/
391+
public RefreshTokenGrantBuilder eventPublisher(ApplicationEventPublisher eventPublisher) {
392+
this.eventPublisher = eventPublisher;
393+
return this;
394+
}
395+
382396
/**
383397
* Sets the maximum acceptable clock skew, which is used when checking the access
384398
* token expiry. An access token is considered expired if
@@ -414,6 +428,9 @@ public OAuth2AuthorizedClientProvider build() {
414428
if (this.accessTokenResponseClient != null) {
415429
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
416430
}
431+
if (this.eventPublisher != null) {
432+
authorizedClientProvider.setApplicationEventPublisher(this.eventPublisher);
433+
}
417434
if (this.clockSkew != null) {
418435
authorizedClientProvider.setClockSkew(this.clockSkew);
419436
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@
2424
import java.util.HashSet;
2525
import java.util.Set;
2626

27+
import org.springframework.context.ApplicationEventPublisher;
28+
import org.springframework.context.ApplicationEventPublisherAware;
2729
import org.springframework.lang.Nullable;
2830
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
2931
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
3032
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
33+
import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent;
3134
import org.springframework.security.oauth2.core.AuthorizationGrantType;
3235
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
3336
import org.springframework.security.oauth2.core.OAuth2Token;
@@ -43,10 +46,13 @@
4346
* @see OAuth2AuthorizedClientProvider
4447
* @see DefaultRefreshTokenTokenResponseClient
4548
*/
46-
public final class RefreshTokenOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
49+
public final class RefreshTokenOAuth2AuthorizedClientProvider
50+
implements OAuth2AuthorizedClientProvider, ApplicationEventPublisherAware {
4751

4852
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
4953

54+
private ApplicationEventPublisher eventPublisher;
55+
5056
private Duration clockSkew = Duration.ofSeconds(60);
5157

5258
private Clock clock = Clock.systemUTC();
@@ -91,8 +97,17 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
9197
authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(),
9298
authorizedClient.getRefreshToken(), scopes);
9399
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(authorizedClient, refreshTokenGrantRequest);
94-
return new OAuth2AuthorizedClient(context.getAuthorizedClient().getClientRegistration(),
95-
context.getPrincipal().getName(), tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
100+
101+
OAuth2AuthorizedClient updatedOAuth2AuthorizedClient = new OAuth2AuthorizedClient(
102+
authorizedClient.getClientRegistration(), context.getPrincipal().getName(),
103+
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
104+
105+
if (this.eventPublisher != null) {
106+
this.eventPublisher
107+
.publishEvent(new OAuth2TokenRefreshedEvent(this, updatedOAuth2AuthorizedClient, tokenResponse));
108+
}
109+
110+
return updatedOAuth2AuthorizedClient;
96111
}
97112

98113
private OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient,
@@ -149,4 +164,9 @@ public void setClock(Clock clock) {
149164
this.clock = clock;
150165
}
151166

167+
@Override
168+
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
169+
this.eventPublisher = applicationEventPublisher;
170+
}
171+
152172
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2025 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.oauth2.client.event;
18+
19+
import org.springframework.context.ApplicationEvent;
20+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
21+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
22+
23+
/**
24+
* An event that is published when an OAuth2 access token is refreshed.
25+
*/
26+
public class OAuth2TokenRefreshedEvent extends ApplicationEvent {
27+
28+
private final OAuth2AuthorizedClient authorizedClient;
29+
30+
private final OAuth2AccessTokenResponse accessTokenResponse;
31+
32+
public OAuth2TokenRefreshedEvent(Object source, OAuth2AuthorizedClient authorizedClient,
33+
OAuth2AccessTokenResponse accessTokenResponse) {
34+
super(source);
35+
this.authorizedClient = authorizedClient;
36+
this.accessTokenResponse = accessTokenResponse;
37+
}
38+
39+
public OAuth2AuthorizedClient getAuthorizedClient() {
40+
return this.authorizedClient;
41+
}
42+
43+
public OAuth2AccessTokenResponse getAccessTokenResponse() {
44+
return this.accessTokenResponse;
45+
}
46+
47+
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ public boolean supports(Class<?> authentication) {
232232
return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
233233
}
234234

235-
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
235+
protected OidcIdToken createOidcToken(ClientRegistration clientRegistration,
236236
OAuth2AccessTokenResponse accessTokenResponse) {
237237
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
238238
Jwt jwt = getJwt(accessTokenResponse, jwtDecoder);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2025 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.oauth2.client.oidc.authentication;
18+
19+
import org.springframework.context.ApplicationListener;
20+
import org.springframework.security.core.Authentication;
21+
import org.springframework.security.core.context.SecurityContextHolder;
22+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
23+
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
24+
import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
26+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
27+
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
28+
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
29+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
30+
31+
/**
32+
* An {@link ApplicationListener} that listens for {@link OAuth2TokenRefreshedEvent}s
33+
*/
34+
public class RefreshOidcIdTokenHandler implements ApplicationListener<OAuth2TokenRefreshedEvent> {
35+
36+
private final OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider;
37+
38+
public RefreshOidcIdTokenHandler(
39+
OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider) {
40+
this.oidcAuthorizationCodeAuthenticationProvider = oidcAuthorizationCodeAuthenticationProvider;
41+
}
42+
43+
@Override
44+
public void onApplicationEvent(OAuth2TokenRefreshedEvent event) {
45+
OAuth2AuthorizedClient authorizedClient = event.getAuthorizedClient();
46+
OAuth2AccessTokenResponse accessTokenResponse = event.getAccessTokenResponse();
47+
OidcIdToken refreshedOidcToken = this.oidcAuthorizationCodeAuthenticationProvider
48+
.createOidcToken(authorizedClient.getClientRegistration(), accessTokenResponse);
49+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
50+
if (authentication instanceof OAuth2AuthenticationToken oauth2AuthenticationToken) {
51+
if (authentication.getPrincipal() instanceof DefaultOidcUser defaultOidcUser) {
52+
OidcUser oidcUser = new DefaultOidcUser(defaultOidcUser.getAuthorities(), refreshedOidcToken,
53+
defaultOidcUser.getUserInfo(), StandardClaimNames.SUB);
54+
SecurityContextHolder.getContext()
55+
.setAuthentication(new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
56+
oauth2AuthenticationToken.getAuthorizedClientRegistrationId()));
57+
}
58+
}
59+
}
60+
61+
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
import org.junit.jupiter.api.Test;
2626
import org.mockito.ArgumentCaptor;
2727

28+
import org.springframework.context.ApplicationEventPublisher;
2829
import org.springframework.security.authentication.TestingAuthenticationToken;
2930
import org.springframework.security.core.Authentication;
3031
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
3132
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
33+
import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent;
3234
import org.springframework.security.oauth2.client.registration.ClientRegistration;
3335
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
3436
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -251,4 +253,55 @@ public void authorizeWhenAuthorizedAndInvalidRequestScopeProvidedThenThrowIllega
251253
+ OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'");
252254
}
253255

256+
@Test
257+
public void shouldPublishEventWhenTokenRefreshed() {
258+
OAuth2TokenRefreshedAwareEventPublisher eventPublisher = new OAuth2TokenRefreshedAwareEventPublisher();
259+
this.authorizedClientProvider.setApplicationEventPublisher(eventPublisher);
260+
// @formatter:off
261+
OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses
262+
.accessTokenResponse()
263+
.refreshToken("new-refresh-token")
264+
.build();
265+
// @formatter:on
266+
given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse);
267+
// @formatter:off
268+
OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext
269+
.withAuthorizedClient(this.authorizedClient)
270+
.principal(this.principal)
271+
.build();
272+
// @formatter:on
273+
this.authorizedClientProvider.authorize(authorizationContext);
274+
assertThat(eventPublisher.flag).isTrue();
275+
}
276+
277+
@Test
278+
public void shouldNotPublishEventWhenTokenNotRefreshed() {
279+
OAuth2TokenRefreshedAwareEventPublisher eventPublisher = new OAuth2TokenRefreshedAwareEventPublisher();
280+
this.authorizedClientProvider.setApplicationEventPublisher(eventPublisher);
281+
282+
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration,
283+
this.principal.getName(), TestOAuth2AccessTokens.noScopes(), this.authorizedClient.getRefreshToken());
284+
// @formatter:off
285+
OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext
286+
.withAuthorizedClient(authorizedClient)
287+
.principal(this.principal)
288+
.build();
289+
// @formatter:on
290+
this.authorizedClientProvider.authorize(authorizationContext);
291+
assertThat(eventPublisher.flag).isFalse();
292+
}
293+
294+
private static class OAuth2TokenRefreshedAwareEventPublisher implements ApplicationEventPublisher {
295+
296+
Boolean flag = false;
297+
298+
@Override
299+
public void publishEvent(Object event) {
300+
if (OAuth2TokenRefreshedEvent.class.isAssignableFrom(event.getClass())) {
301+
this.flag = true;
302+
}
303+
}
304+
305+
}
306+
254307
}

0 commit comments

Comments
 (0)