From 8f7b48e6bae7ef8ef61d0ec443ac551f0759f772 Mon Sep 17 00:00:00 2001 From: Hao Date: Fri, 14 Feb 2025 00:08:50 +0800 Subject: [PATCH 1/3] Ensure ID Token is updated after refresh token Signed-off-by: Hao --- .../OAuth2ClientConfiguration.java | 16 ++++- .../oauth2/client/OAuth2LoginConfigurer.java | 5 ++ ...Auth2AuthorizedClientManagerRegistrar.java | 7 +++ ...OAuth2AuthorizedClientProviderBuilder.java | 17 ++++++ ...shTokenOAuth2AuthorizedClientProvider.java | 26 +++++++- .../event/OAuth2TokenRefreshedEvent.java | 47 ++++++++++++++ ...thorizationCodeAuthenticationProvider.java | 2 +- .../RefreshOidcIdTokenHandler.java | 61 +++++++++++++++++++ ...enOAuth2AuthorizedClientProviderTests.java | 53 ++++++++++++++++ 9 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java index 13c9a1b3c07..55de62810d5 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java @@ -34,6 +34,9 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -160,7 +163,7 @@ private OAuth2AuthorizedClientManager getAuthorizedClientManager() { * @since 6.2.0 */ static final class OAuth2AuthorizedClientManagerRegistrar - implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware { + implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, BeanFactoryAware { static final String BEAN_NAME = "authorizedClientManagerRegistrar"; @@ -179,6 +182,8 @@ static final class OAuth2AuthorizedClientManagerRegistrar private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); + private ApplicationEventPublisher eventPublisher; + private ListableBeanFactory beanFactory; @Override @@ -302,6 +307,10 @@ private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider( authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); } + if (this.eventPublisher != null) { + authorizedClientProvider.setApplicationEventPublisher(this.eventPublisher); + } + return authorizedClientProvider; } @@ -423,6 +432,11 @@ private T getBeanOfType(ResolvableType resolvableType) { return objectProvider.getIfAvailable(); } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.eventPublisher = applicationContext; + } + } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 4c53b3293d0..68cc0dd0bf2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -56,6 +56,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.oidc.authentication.RefreshOidcIdTokenHandler; import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry; import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation; import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry; @@ -393,6 +394,10 @@ public void init(B http) throws Exception { oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper); } http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider)); + + RefreshOidcIdTokenHandler refreshOidcIdTokenHandler = new RefreshOidcIdTokenHandler( + oidcAuthorizationCodeAuthenticationProvider); + registerDelegateApplicationListener(refreshOidcIdTokenHandler); } else { http.authenticationProvider(new OidcAuthenticationRequestChecker()); diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java b/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java index 669d6f7f67f..d2252435f7b 100644 --- a/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.AnnotationBeanNameGenerator; import org.springframework.core.ResolvableType; import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider; @@ -197,6 +198,12 @@ private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider( authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient); } + ApplicationEventPublisher applicationEventPublisher = getBeanOfType( + ResolvableType.forClass(ApplicationEventPublisher.class)); + if (applicationEventPublisher != null) { + authorizedClientProvider.setApplicationEventPublisher(applicationEventPublisher); + } + return authorizedClientProvider; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java index c0c8bee93ee..bcd130063e6 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.function.Consumer; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest; @@ -359,6 +360,8 @@ public final class RefreshTokenGrantBuilder implements Builder { private OAuth2AccessTokenResponseClient accessTokenResponseClient; + private ApplicationEventPublisher eventPublisher; + private Duration clockSkew; private Clock clock; @@ -379,6 +382,17 @@ public RefreshTokenGrantBuilder accessTokenResponseClient( return this; } + /** + * Sets the {@link ApplicationEventPublisher} used when an access token is + * refreshed. + * @param eventPublisher the {@link ApplicationEventPublisher} + * @return the {@link RefreshTokenGrantBuilder} + */ + public RefreshTokenGrantBuilder eventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + return this; + } + /** * Sets the maximum acceptable clock skew, which is used when checking the access * token expiry. An access token is considered expired if @@ -414,6 +428,9 @@ public OAuth2AuthorizedClientProvider build() { if (this.accessTokenResponseClient != null) { authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient); } + if (this.eventPublisher != null) { + authorizedClientProvider.setApplicationEventPublisher(this.eventPublisher); + } if (this.clockSkew != null) { authorizedClientProvider.setClockSkew(this.clockSkew); } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java index 410a33fda18..17dc2ad16b9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java @@ -24,10 +24,13 @@ import java.util.HashSet; import java.util.Set; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import org.springframework.security.oauth2.core.OAuth2Token; @@ -43,10 +46,13 @@ * @see OAuth2AuthorizedClientProvider * @see DefaultRefreshTokenTokenResponseClient */ -public final class RefreshTokenOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { +public final class RefreshTokenOAuth2AuthorizedClientProvider + implements OAuth2AuthorizedClientProvider, ApplicationEventPublisherAware { private OAuth2AccessTokenResponseClient accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient(); + private ApplicationEventPublisher eventPublisher; + private Duration clockSkew = Duration.ofSeconds(60); private Clock clock = Clock.systemUTC(); @@ -91,8 +97,17 @@ public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(), authorizedClient.getRefreshToken(), scopes); OAuth2AccessTokenResponse tokenResponse = getTokenResponse(authorizedClient, refreshTokenGrantRequest); - return new OAuth2AuthorizedClient(context.getAuthorizedClient().getClientRegistration(), - context.getPrincipal().getName(), tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + + OAuth2AuthorizedClient updatedOAuth2AuthorizedClient = new OAuth2AuthorizedClient( + authorizedClient.getClientRegistration(), context.getPrincipal().getName(), + tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + + if (this.eventPublisher != null) { + this.eventPublisher + .publishEvent(new OAuth2TokenRefreshedEvent(this, updatedOAuth2AuthorizedClient, tokenResponse)); + } + + return updatedOAuth2AuthorizedClient; } private OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient, @@ -149,4 +164,9 @@ public void setClock(Clock clock) { this.clock = clock; } + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } + } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java new file mode 100644 index 00000000000..f92091d4cd7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; + +/** + * An event that is published when an OAuth2 access token is refreshed. + */ +public class OAuth2TokenRefreshedEvent extends ApplicationEvent { + + private final OAuth2AuthorizedClient authorizedClient; + + private final OAuth2AccessTokenResponse accessTokenResponse; + + public OAuth2TokenRefreshedEvent(Object source, OAuth2AuthorizedClient authorizedClient, + OAuth2AccessTokenResponse accessTokenResponse) { + super(source); + this.authorizedClient = authorizedClient; + this.accessTokenResponse = accessTokenResponse; + } + + public OAuth2AuthorizedClient getAuthorizedClient() { + return this.authorizedClient; + } + + public OAuth2AccessTokenResponse getAccessTokenResponse() { + return this.accessTokenResponse; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 64cfba6816a..30ec3945ee5 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -232,7 +232,7 @@ public boolean supports(Class authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } - private OidcIdToken createOidcToken(ClientRegistration clientRegistration, + protected OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); Jwt jwt = getJwt(accessTokenResponse, jwtDecoder); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java new file mode 100644 index 00000000000..304c556babe --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.authentication; + +import org.springframework.context.ApplicationListener; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +/** + * An {@link ApplicationListener} that listens for {@link OAuth2TokenRefreshedEvent}s + */ +public class RefreshOidcIdTokenHandler implements ApplicationListener { + + private final OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider; + + public RefreshOidcIdTokenHandler( + OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider) { + this.oidcAuthorizationCodeAuthenticationProvider = oidcAuthorizationCodeAuthenticationProvider; + } + + @Override + public void onApplicationEvent(OAuth2TokenRefreshedEvent event) { + OAuth2AuthorizedClient authorizedClient = event.getAuthorizedClient(); + OAuth2AccessTokenResponse accessTokenResponse = event.getAccessTokenResponse(); + OidcIdToken refreshedOidcToken = this.oidcAuthorizationCodeAuthenticationProvider + .createOidcToken(authorizedClient.getClientRegistration(), accessTokenResponse); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication instanceof OAuth2AuthenticationToken oauth2AuthenticationToken) { + if (authentication.getPrincipal() instanceof DefaultOidcUser defaultOidcUser) { + OidcUser oidcUser = new DefaultOidcUser(defaultOidcUser.getAuthorities(), refreshedOidcToken, + defaultOidcUser.getUserInfo(), StandardClaimNames.SUB); + SecurityContextHolder.getContext() + .setAuthentication(new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), + oauth2AuthenticationToken.getAuthorizedClientRegistrationId())); + } + } + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java index 86ae003eff2..dc4c4232004 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java @@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest; +import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.core.OAuth2AccessToken; @@ -251,4 +253,55 @@ public void authorizeWhenAuthorizedAndInvalidRequestScopeProvidedThenThrowIllega + OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'"); } + @Test + public void shouldPublishEventWhenTokenRefreshed() { + OAuth2TokenRefreshedAwareEventPublisher eventPublisher = new OAuth2TokenRefreshedAwareEventPublisher(); + this.authorizedClientProvider.setApplicationEventPublisher(eventPublisher); + // @formatter:off + OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses + .accessTokenResponse() + .refreshToken("new-refresh-token") + .build(); + // @formatter:on + given(this.accessTokenResponseClient.getTokenResponse(any())).willReturn(accessTokenResponse); + // @formatter:off + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withAuthorizedClient(this.authorizedClient) + .principal(this.principal) + .build(); + // @formatter:on + this.authorizedClientProvider.authorize(authorizationContext); + assertThat(eventPublisher.flag).isTrue(); + } + + @Test + public void shouldNotPublishEventWhenTokenNotRefreshed() { + OAuth2TokenRefreshedAwareEventPublisher eventPublisher = new OAuth2TokenRefreshedAwareEventPublisher(); + this.authorizedClientProvider.setApplicationEventPublisher(eventPublisher); + + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistration, + this.principal.getName(), TestOAuth2AccessTokens.noScopes(), this.authorizedClient.getRefreshToken()); + // @formatter:off + OAuth2AuthorizationContext authorizationContext = OAuth2AuthorizationContext + .withAuthorizedClient(authorizedClient) + .principal(this.principal) + .build(); + // @formatter:on + this.authorizedClientProvider.authorize(authorizationContext); + assertThat(eventPublisher.flag).isFalse(); + } + + private static class OAuth2TokenRefreshedAwareEventPublisher implements ApplicationEventPublisher { + + Boolean flag = false; + + @Override + public void publishEvent(Object event) { + if (OAuth2TokenRefreshedEvent.class.isAssignableFrom(event.getClass())) { + this.flag = true; + } + } + + } + } From c037fff3d6a2b04585c680c66857a1ca754338a2 Mon Sep 17 00:00:00 2001 From: Hao Date: Sat, 15 Feb 2025 02:08:42 +0800 Subject: [PATCH 2/3] Update RefreshOidcIdTokenHandler to improve decoupling Signed-off-by: Hao --- .../oauth2/client/OAuth2LoginConfigurer.java | 9 ++- ...thorizationCodeAuthenticationProvider.java | 2 +- .../RefreshOidcIdTokenHandler.java | 74 ++++++++++++++++--- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 68cc0dd0bf2..34ab06415e6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -395,8 +395,13 @@ public void init(B http) throws Exception { } http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider)); - RefreshOidcIdTokenHandler refreshOidcIdTokenHandler = new RefreshOidcIdTokenHandler( - oidcAuthorizationCodeAuthenticationProvider); + RefreshOidcIdTokenHandler refreshOidcIdTokenHandler = new RefreshOidcIdTokenHandler(); + if (this.getSecurityContextHolderStrategy() != null) { + refreshOidcIdTokenHandler.setSecurityContextHolderStrategy(this.getSecurityContextHolderStrategy()); + } + if (jwtDecoderFactory != null) { + refreshOidcIdTokenHandler.setJwtDecoderFactory(jwtDecoderFactory); + } registerDelegateApplicationListener(refreshOidcIdTokenHandler); } else { diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java index 30ec3945ee5..64cfba6816a 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java @@ -232,7 +232,7 @@ public boolean supports(Class authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } - protected OidcIdToken createOidcToken(ClientRegistration clientRegistration, + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); Jwt jwt = getJwt(accessTokenResponse, jwtDecoder); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java index 304c556babe..f567f87b2ed 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java @@ -16,46 +16,98 @@ package org.springframework.security.oauth2.client.oidc.authentication; +import java.util.Map; + import org.springframework.context.ApplicationListener; import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.util.Assert; /** * An {@link ApplicationListener} that listens for {@link OAuth2TokenRefreshedEvent}s */ public class RefreshOidcIdTokenHandler implements ApplicationListener { - private final OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider; + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; - public RefreshOidcIdTokenHandler( - OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider) { - this.oidcAuthorizationCodeAuthenticationProvider = oidcAuthorizationCodeAuthenticationProvider; - } + private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder + .getContextHolderStrategy(); + + private JwtDecoderFactory jwtDecoderFactory = new OidcIdTokenDecoderFactory(); @Override public void onApplicationEvent(OAuth2TokenRefreshedEvent event) { OAuth2AuthorizedClient authorizedClient = event.getAuthorizedClient(); OAuth2AccessTokenResponse accessTokenResponse = event.getAccessTokenResponse(); - OidcIdToken refreshedOidcToken = this.oidcAuthorizationCodeAuthenticationProvider - .createOidcToken(authorizedClient.getClientRegistration(), accessTokenResponse); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); + OidcIdToken refreshedOidcToken = createOidcToken(clientRegistration, accessTokenResponse); + Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication(); if (authentication instanceof OAuth2AuthenticationToken oauth2AuthenticationToken) { if (authentication.getPrincipal() instanceof DefaultOidcUser defaultOidcUser) { OidcUser oidcUser = new DefaultOidcUser(defaultOidcUser.getAuthorities(), refreshedOidcToken, defaultOidcUser.getUserInfo(), StandardClaimNames.SUB); - SecurityContextHolder.getContext() - .setAuthentication(new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), - oauth2AuthenticationToken.getAuthorizedClientRegistrationId())); + SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); + context.setAuthentication(new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), + oauth2AuthenticationToken.getAuthorizedClientRegistrationId())); + this.securityContextHolderStrategy.setContext(context); } } } + /** + * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use + * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}. + */ + public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) { + this.securityContextHolderStrategy = securityContextHolderStrategy; + } + + /** + * Sets the {@link JwtDecoderFactory} used for {@link OidcIdToken} signature + * verification. The factory returns a {@link JwtDecoder} associated to the provided + * {@link ClientRegistration}. + * @param jwtDecoderFactory the {@link JwtDecoderFactory} used for {@link OidcIdToken} + * signature verification + */ + public final void setJwtDecoderFactory(JwtDecoderFactory jwtDecoderFactory) { + Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null"); + this.jwtDecoderFactory = jwtDecoderFactory; + } + + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, + OAuth2AccessTokenResponse accessTokenResponse) { + JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); + Jwt jwt = getJwt(accessTokenResponse, jwtDecoder); + return new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + } + + private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) { + try { + Map parameters = accessTokenResponse.getAdditionalParameters(); + return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN)); + } + catch (JwtException ex) { + OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null); + throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex); + } + } + } From ef9896d4e1190d411b6279d958c5165b762c58d0 Mon Sep 17 00:00:00 2001 From: Hao Date: Sat, 15 Feb 2025 21:49:29 +0800 Subject: [PATCH 3/3] Refactor RefreshOidcIdTokenHandler and Add tests Signed-off-by: Hao --- .../RefreshOidcIdTokenHandler.java | 48 ++- .../RefreshOidcIdTokenHandlerTests.java | 284 ++++++++++++++++++ 2 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java index f567f87b2ed..d1af1a4f485 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java @@ -31,6 +31,7 @@ import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.StandardClaimNames; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; @@ -46,6 +47,8 @@ */ public class RefreshOidcIdTokenHandler implements ApplicationListener { + private static final String MISSING_ID_TOKEN_ERROR_CODE = "missing_id_token"; + private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token"; private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder @@ -56,20 +59,31 @@ public class RefreshOidcIdTokenHandler implements ApplicationListener jwt this.jwtDecoderFactory = jwtDecoderFactory; } + private void updateSecurityContext(OAuth2AuthenticationToken oauth2Authentication, DefaultOidcUser defaultOidcUser, + OidcIdToken refreshedOidcToken) { + OidcUser oidcUser = new DefaultOidcUser(defaultOidcUser.getAuthorities(), refreshedOidcToken, + defaultOidcUser.getUserInfo(), StandardClaimNames.SUB); + + SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); + context.setAuthentication(new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(), + oauth2Authentication.getAuthorizedClientRegistrationId())); + + this.securityContextHolderStrategy.setContext(context); + } + private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java new file mode 100644 index 00000000000..61ff5aae892 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.oidc.authentication; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.event.OAuth2TokenRefreshedEvent; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.oauth2.jwt.JwtException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class RefreshOidcIdTokenHandlerTests { + + private static final String EXISTING_ID_TOKEN_VALUE = "id-token-value"; + + private static final String REFRESHED_ID_TOKEN_VALUE = "new-id-token-value"; + + private static final String EXISTING_ACCESS_TOKEN_VALUE = "token-value"; + + private static final String REFRESHED_ACCESS_TOKEN_VALUE = "new-token-value"; + + private RefreshOidcIdTokenHandler handler; + + private RefreshTokenOAuth2AuthorizedClientProvider provider; + + private ClientRegistration clientRegistration; + + private OAuth2AuthorizedClient authorizedClient; + + private JwtDecoder jwtDecoder; + + private SecurityContext securityContext; + + private OidcIdToken existingIdToken; + + @BeforeEach + void setUp() { + this.handler = new RefreshOidcIdTokenHandler(); + + this.clientRegistration = createClientRegistrationWithScopes(OidcScopes.OPENID); + this.authorizedClient = createAuthorizedClient(this.clientRegistration); + + this.provider = mock(RefreshTokenOAuth2AuthorizedClientProvider.class); + + JwtDecoderFactory jwtDecoderFactory = mock(JwtDecoderFactory.class); + this.jwtDecoder = mock(JwtDecoder.class); + SecurityContextHolderStrategy securityContextHolderStrategy = mock(SecurityContextHolderStrategy.class); + this.securityContext = mock(SecurityContext.class); + + this.handler.setJwtDecoderFactory(jwtDecoderFactory); + this.handler.setSecurityContextHolderStrategy(securityContextHolderStrategy); + + given(jwtDecoderFactory.createDecoder(any())).willReturn(this.jwtDecoder); + given(securityContextHolderStrategy.createEmptyContext()).willReturn(this.securityContext); + given(securityContextHolderStrategy.getContext()).willReturn(this.securityContext); + + Map claims = new HashMap<>(); + claims.put("sub", "subject"); + Jwt existingIdTokenJwt = new Jwt(EXISTING_ID_TOKEN_VALUE, Instant.now(), Instant.now().plusSeconds(3600), + Map.of("alg", "RS256"), claims); + Jwt refreshedIdTokenJwt = new Jwt(REFRESHED_ID_TOKEN_VALUE, Instant.now(), Instant.now().plusSeconds(3600), + Map.of("alg", "RS256"), claims); + + this.existingIdToken = new OidcIdToken(existingIdTokenJwt.getTokenValue(), existingIdTokenJwt.getIssuedAt(), + existingIdTokenJwt.getExpiresAt(), existingIdTokenJwt.getClaims()); + + given(this.jwtDecoder.decode(existingIdTokenJwt.getTokenValue())).willReturn(existingIdTokenJwt); + given(this.jwtDecoder.decode(refreshedIdTokenJwt.getTokenValue())).willReturn(refreshedIdTokenJwt); + } + + @Test + void handleEventWhenValidIdTokenThenUpdatesSecurityContext() { + + DefaultOidcUser existingUser = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), + this.existingIdToken); + OAuth2AuthenticationToken existingAuth = new OAuth2AuthenticationToken(existingUser, + existingUser.getAuthorities(), "registration-id"); + given(this.securityContext.getAuthentication()).willReturn(existingAuth); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(Map.of(OidcParameterNames.ID_TOKEN, REFRESHED_ID_TOKEN_VALUE)) + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + this.handler.onApplicationEvent(event); + + ArgumentCaptor authenticationCaptor = ArgumentCaptor + .forClass(OAuth2AuthenticationToken.class); + verify(this.securityContext).setAuthentication(authenticationCaptor.capture()); + + OAuth2AuthenticationToken newAuthentication = authenticationCaptor.getValue(); + assertThat(newAuthentication.getPrincipal()).isInstanceOf(DefaultOidcUser.class); + DefaultOidcUser newUser = (DefaultOidcUser) newAuthentication.getPrincipal(); + assertThat(newUser.getIdToken().getTokenValue()).isEqualTo(REFRESHED_ID_TOKEN_VALUE); + } + + @Test + void handleEventWhenAuthorizedClientIsNotOidcThenDoesNothing() { + + this.clientRegistration = createClientRegistrationWithScopes("read"); + this.authorizedClient = createAuthorizedClient(this.clientRegistration); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(Map.of(OidcParameterNames.ID_TOKEN, REFRESHED_ID_TOKEN_VALUE)) + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + + this.handler.onApplicationEvent(event); + + verify(this.securityContext, never()).setAuthentication(any()); + verify(this.jwtDecoder, never()).decode(any()); + } + + @Test + void handleEventWhenAuthenticationNotOAuth2AuthenticationTokenThenDoesNothing() { + + given(this.securityContext.getAuthentication()).willReturn(mock(TestingAuthenticationToken.class)); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(Map.of(OidcParameterNames.ID_TOKEN, REFRESHED_ID_TOKEN_VALUE)) + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + + this.handler.onApplicationEvent(event); + + verify(this.securityContext, never()).setAuthentication(any()); + } + + @Test + void handleEventWhenNotOidcUserThenDoesNothing() { + + OAuth2AuthenticationToken existingAuth = new OAuth2AuthenticationToken( + new DefaultOAuth2User(Collections.emptySet(), + Collections.singletonMap("custom-attribute", "test-subject"), "custom-attribute"), + AuthorityUtils.createAuthorityList("ROLE_USER"), "registration-id"); + given(this.securityContext.getAuthentication()).willReturn(existingAuth); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(Map.of(OidcParameterNames.ID_TOKEN, REFRESHED_ID_TOKEN_VALUE)) + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + + this.handler.onApplicationEvent(event); + + verify(this.securityContext, never()).setAuthentication(any()); + } + + @Test + void handleEventWhenMissingIdTokenThenThrowsException() { + + DefaultOidcUser existingUser = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), + this.existingIdToken); + OAuth2AuthenticationToken existingAuth = new OAuth2AuthenticationToken(existingUser, + existingUser.getAuthorities(), "registration-id"); + given(this.securityContext.getAuthentication()).willReturn(existingAuth); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(new HashMap<>()) // missing ID token + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> this.handler.onApplicationEvent(event)) + .withMessageContaining("missing_id_token"); + } + + @Test + void handleEventWhenInvalidIdTokenThenThrowsException() { + + DefaultOidcUser existingUser = new DefaultOidcUser(AuthorityUtils.createAuthorityList("ROLE_USER"), + this.existingIdToken); + OAuth2AuthenticationToken existingAuth = new OAuth2AuthenticationToken(existingUser, + existingUser.getAuthorities(), "registration-id"); + given(this.securityContext.getAuthentication()).willReturn(existingAuth); + + given(this.jwtDecoder.decode(any())).willThrow(new JwtException("Invalid token")); + + OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse + .withToken(REFRESHED_ACCESS_TOKEN_VALUE) + .tokenType(OAuth2AccessToken.TokenType.BEARER) + .expiresIn(3600) + .additionalParameters(Map.of(OidcParameterNames.ID_TOKEN, "invalid-id-token")) + .build(); + + OAuth2TokenRefreshedEvent event = new OAuth2TokenRefreshedEvent(this.provider, this.authorizedClient, + accessTokenResponse); + + assertThatExceptionOfType(OAuth2AuthenticationException.class) + .isThrownBy(() -> this.handler.onApplicationEvent(event)) + .withMessageContaining("invalid_id_token"); + } + + private ClientRegistration createClientRegistrationWithScopes(String... scope) { + return ClientRegistration.withRegistrationId("registration-id") + .clientId("client-id") + .clientSecret("secret") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("http://localhost") + .scope(scope) + .authorizationUri("https://provider.com/oauth2/authorize") + .tokenUri("https://provider.com/oauth2/token") + .jwkSetUri("https://provider.com/jwk") + .userInfoUri("https://provider.com/user") + .build(); + } + + private static OAuth2AuthorizedClient createAuthorizedClient(ClientRegistration clientRegistration) { + return new OAuth2AuthorizedClient(clientRegistration, "principal-name", + new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, EXISTING_ACCESS_TOKEN_VALUE, Instant.now(), + Instant.now().plusSeconds(3600))); + } + +}