瀏覽代碼

Polish gh-16589

Steve Riesenberg 6 月之前
父節點
當前提交
5bb5d0f6be
共有 16 個文件被更改,包括 1580 次插入503 次删除
  1. 8 9
      config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java
  2. 24 10
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
  3. 86 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListener.java
  4. 1 1
      config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java
  5. 504 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListenerConfigurationTests.java
  6. 135 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListenerTests.java
  7. 2 1
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2AuthorizedClientProviderBuilder.java
  8. 11 9
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProvider.java
  9. 69 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2AuthorizedClientRefreshedEvent.java
  10. 0 47
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java
  11. 219 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java
  12. 0 139
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java
  13. 98 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/OidcUserRefreshedEvent.java
  14. 3 3
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshTokenOAuth2AuthorizedClientProviderTests.java
  15. 420 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListenerTests.java
  16. 0 284
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java

+ 8 - 9
config/src/main/java/org/springframework/security/config/annotation/web/configuration/OAuth2ClientConfiguration.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * 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.
@@ -34,9 +34,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
 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.ApplicationEventPublisherAware;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -163,7 +162,7 @@ final class OAuth2ClientConfiguration {
 	 * @since 6.2.0
 	 */
 	static final class OAuth2AuthorizedClientManagerRegistrar
-			implements ApplicationContextAware, BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
+			implements ApplicationEventPublisherAware, BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
 
 		static final String BEAN_NAME = "authorizedClientManagerRegistrar";
 
@@ -182,7 +181,7 @@ final class OAuth2ClientConfiguration {
 
 		private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
 
-		private ApplicationEventPublisher eventPublisher;
+		private ApplicationEventPublisher applicationEventPublisher;
 
 		private ListableBeanFactory beanFactory;
 
@@ -307,8 +306,8 @@ final class OAuth2ClientConfiguration {
 				authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
 			}
 
-			if (this.eventPublisher != null) {
-				authorizedClientProvider.setApplicationEventPublisher(this.eventPublisher);
+			if (this.applicationEventPublisher != null) {
+				authorizedClientProvider.setApplicationEventPublisher(this.applicationEventPublisher);
 			}
 
 			return authorizedClientProvider;
@@ -433,8 +432,8 @@ final class OAuth2ClientConfiguration {
 		}
 
 		@Override
-		public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
-			this.eventPublisher = applicationContext;
+		public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+			this.applicationEventPublisher = applicationEventPublisher;
 		}
 
 	}

+ 24 - 10
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

@@ -57,7 +57,7 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC
 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.authentication.OidcAuthorizedClientRefreshedEventListener;
 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;
@@ -91,6 +91,7 @@ import org.springframework.security.web.authentication.LoginUrlAuthenticationEnt
 import org.springframework.security.web.authentication.session.SessionAuthenticationException;
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
 import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
+import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.csrf.CsrfToken;
 import org.springframework.security.web.savedrequest.RequestCache;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
@@ -387,23 +388,26 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 			OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService = getOidcUserService();
 			OidcAuthorizationCodeAuthenticationProvider oidcAuthorizationCodeAuthenticationProvider = new OidcAuthorizationCodeAuthenticationProvider(
 					accessTokenResponseClient, oidcUserService);
+			OidcAuthorizedClientRefreshedEventListener oidcAuthorizedClientRefreshedEventListener = new OidcAuthorizedClientRefreshedEventListener();
+			oidcAuthorizedClientRefreshedEventListener.setUserService(oidcUserService);
+			oidcAuthorizedClientRefreshedEventListener
+				.setApplicationEventPublisher(http.getSharedObject(ApplicationContext.class));
+
 			JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = this.getJwtDecoderFactoryBean();
 			if (jwtDecoderFactory != null) {
 				oidcAuthorizationCodeAuthenticationProvider.setJwtDecoderFactory(jwtDecoderFactory);
+				oidcAuthorizedClientRefreshedEventListener.setJwtDecoderFactory(jwtDecoderFactory);
 			}
 			if (userAuthoritiesMapper != null) {
 				oidcAuthorizationCodeAuthenticationProvider.setAuthoritiesMapper(userAuthoritiesMapper);
+				oidcAuthorizedClientRefreshedEventListener.setAuthoritiesMapper(userAuthoritiesMapper);
 			}
-			http.authenticationProvider(this.postProcess(oidcAuthorizationCodeAuthenticationProvider));
+			oidcAuthorizationCodeAuthenticationProvider = this.postProcess(oidcAuthorizationCodeAuthenticationProvider);
+			http.authenticationProvider(oidcAuthorizationCodeAuthenticationProvider);
 
-			RefreshOidcIdTokenHandler refreshOidcIdTokenHandler = new RefreshOidcIdTokenHandler();
-			if (this.getSecurityContextHolderStrategy() != null) {
-				refreshOidcIdTokenHandler.setSecurityContextHolderStrategy(this.getSecurityContextHolderStrategy());
-			}
-			if (jwtDecoderFactory != null) {
-				refreshOidcIdTokenHandler.setJwtDecoderFactory(jwtDecoderFactory);
-			}
-			registerDelegateApplicationListener(refreshOidcIdTokenHandler);
+			oidcAuthorizedClientRefreshedEventListener = this.postProcess(oidcAuthorizedClientRefreshedEventListener);
+			registerDelegateApplicationListener(oidcAuthorizedClientRefreshedEventListener);
+			configureOidcUserRefreshedEventListener(http);
 		}
 		else {
 			http.authenticationProvider(new OidcAuthenticationRequestChecker());
@@ -631,6 +635,16 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 		registerDelegateApplicationListener(listener);
 	}
 
+	private void configureOidcUserRefreshedEventListener(B http) {
+		OidcUserRefreshedEventListener oidcUserRefreshedEventListener = new OidcUserRefreshedEventListener();
+		oidcUserRefreshedEventListener.setSecurityContextHolderStrategy(this.getSecurityContextHolderStrategy());
+		SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
+		if (securityContextRepository != null) {
+			oidcUserRefreshedEventListener.setSecurityContextRepository(securityContextRepository);
+		}
+		registerDelegateApplicationListener(oidcUserRefreshedEventListener);
+	}
+
 	private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
 		DelegatingApplicationListener delegating = getBeanOrNull(
 				ResolvableType.forType(DelegatingApplicationListener.class));

+ 86 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListener.java

@@ -0,0 +1,86 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.client;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.context.ApplicationListener;
+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.oidc.authentication.event.OidcUserRefreshedEvent;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * An {@link ApplicationListener} that listens for events of type
+ * {@link OidcUserRefreshedEvent} and refreshes the {@link SecurityContext}.
+ *
+ * @author Steve Riesenberg
+ * @since 6.5
+ * @see org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider
+ */
+final class OidcUserRefreshedEventListener implements ApplicationListener<OidcUserRefreshedEvent> {
+
+	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
+		.getContextHolderStrategy();
+
+	private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
+
+	@Override
+	public void onApplicationEvent(OidcUserRefreshedEvent event) {
+		SecurityContext securityContext = this.securityContextHolderStrategy.createEmptyContext();
+		securityContext.setAuthentication(event.getAuthentication());
+		this.securityContextHolderStrategy.setContext(securityContext);
+
+		RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+		if (!(requestAttributes instanceof ServletRequestAttributes servletRequestAttributes)) {
+			return;
+		}
+
+		HttpServletRequest request = servletRequestAttributes.getRequest();
+		HttpServletResponse response = servletRequestAttributes.getResponse();
+		this.securityContextRepository.saveContext(securityContext, request, response);
+	}
+
+	/**
+	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
+	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
+	 * @param securityContextHolderStrategy the {@link SecurityContextHolderStrategy} to
+	 * use
+	 */
+	void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
+		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
+		this.securityContextHolderStrategy = securityContextHolderStrategy;
+	}
+
+	/**
+	 * Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} upon
+	 * receiving an {@link OidcUserRefreshedEvent}.
+	 * @param securityContextRepository the {@link SecurityContextRepository} to use
+	 */
+	void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
+		Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
+		this.securityContextRepository = securityContextRepository;
+	}
+
+}

+ 1 - 1
config/src/main/java/org/springframework/security/config/http/OAuth2AuthorizedClientManagerRegistrar.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * 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.

+ 504 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListenerConfigurationTests.java

@@ -0,0 +1,504 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.client;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2RefreshToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
+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;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+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.TestJwts;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link OidcUserRefreshedEventListener} with {@link OAuth2LoginConfigurer}.
+ *
+ * @author Steve Riesenberg
+ */
+public class OidcUserRefreshedEventListenerConfigurationTests {
+
+	// @formatter:off
+	private static final ClientRegistration GOOGLE_CLIENT_REGISTRATION = CommonOAuth2Provider.GOOGLE
+			.getBuilder("google")
+			.clientId("clientId")
+			.clientSecret("clientSecret")
+			.build();
+	// @formatter:on
+
+	// @formatter:off
+	private static final ClientRegistration GITHUB_CLIENT_REGISTRATION = CommonOAuth2Provider.GITHUB
+			.getBuilder("github")
+			.clientId("clientId")
+			.clientSecret("clientSecret")
+			.build();
+	// @formatter:on
+
+	private static final String SUBJECT = "surfer-dude";
+
+	private static final String ACCESS_TOKEN_VALUE = "hang-ten";
+
+	private static final String REFRESH_TOKEN_VALUE = "surfs-up";
+
+	private static final String ID_TOKEN_VALUE = "beach-break";
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	private SecurityContextRepository securityContextRepository;
+
+	@Autowired
+	private OAuth2AuthorizedClientRepository authorizedClientRepository;
+
+	@Autowired
+	private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient;
+
+	@Autowired
+	private JwtDecoder jwtDecoder;
+
+	@Autowired
+	private OidcUserService oidcUserService;
+
+	@Autowired
+	private OAuth2AuthorizedClientManager authorizedClientManager;
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	@BeforeEach
+	public void setUp() {
+		this.request = new MockHttpServletRequest("GET", "");
+		this.request.setServletPath("/");
+		this.response = new MockHttpServletResponse();
+		RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(this.request, this.response));
+	}
+
+	@AfterEach
+	public void cleanUp() {
+		SecurityContextHolder.clearContext();
+		RequestContextHolder.resetRequestAttributes();
+	}
+
+	@Test
+	public void authorizeWhenAccessTokenResponseMissingOpenidScopeThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse();
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenAccessTokenResponseMissingIdTokenThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.oidcAccessTokenResponse()
+			.build();
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenAuthenticationIsNotOAuth2ThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		TestingAuthenticationToken authentication = new TestingAuthenticationToken(SUBJECT, null);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		SecurityContextHolder.setContext(securityContext);
+
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenAuthenticationIsCustomThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		OidcUser oidcUser = createOidcUser();
+		OAuth2AuthenticationToken authentication = new CustomOAuth2AuthenticationToken(oidcUser,
+				oidcUser.getAuthorities(), GOOGLE_CLIENT_REGISTRATION.getRegistrationId());
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		SecurityContextHolder.setContext(securityContext);
+
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenPrincipalIsOAuth2UserThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		Map<String, Object> attributes = Map.of(StandardClaimNames.SUB, SUBJECT);
+		OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("OAUTH2_USER"), attributes,
+				StandardClaimNames.SUB);
+		OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oauth2User,
+				oauth2User.getAuthorities(), GOOGLE_CLIENT_REGISTRATION.getRegistrationId());
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		SecurityContextHolder.setContext(securityContext);
+
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenAuthenticationClientRegistrationIdDoesNotMatchThenOidcUserNotRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(GITHUB_CLIENT_REGISTRATION);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		SecurityContextHolder.setContext(securityContext);
+
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		verifyNoInteractions(this.securityContextRepository, this.jwtDecoder, this.oidcUserService);
+	}
+
+	@Test
+	public void authorizeWhenAccessTokenResponseIncludesIdTokenThenOidcUserRefreshed() {
+		this.spring.register(OAuth2LoginWithOAuth2ClientConfig.class).autowire();
+
+		OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
+		OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		Jwt jwt = createJwt();
+		OidcUser oidcUser = createOidcUser();
+		given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
+				any(HttpServletRequest.class)))
+			.willReturn(authorizedClient);
+		given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
+			.willReturn(accessTokenResponse);
+		given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
+		given(this.oidcUserService.loadUser(any(OidcUserRequest.class))).willReturn(oidcUser);
+
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		SecurityContextHolder.setContext(securityContext);
+
+		OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
+			.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
+			.principal(authentication)
+			.build();
+		OAuth2AuthorizedClient refreshedAuthorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
+		assertThat(refreshedAuthorizedClient).isNotNull();
+		assertThat(refreshedAuthorizedClient).isNotSameAs(authorizedClient);
+		assertThat(refreshedAuthorizedClient.getClientRegistration()).isEqualTo(GOOGLE_CLIENT_REGISTRATION);
+		assertThat(refreshedAuthorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+		assertThat(refreshedAuthorizedClient.getRefreshToken()).isEqualTo(accessTokenResponse.getRefreshToken());
+
+		ArgumentCaptor<OAuth2RefreshTokenGrantRequest> refreshTokenGrantRequestCaptor = ArgumentCaptor
+			.forClass(OAuth2RefreshTokenGrantRequest.class);
+		ArgumentCaptor<OidcUserRequest> userRequestCaptor = ArgumentCaptor.forClass(OidcUserRequest.class);
+		ArgumentCaptor<SecurityContext> securityContextCaptor = ArgumentCaptor.forClass(SecurityContext.class);
+		verify(this.authorizedClientRepository).loadAuthorizedClient(GOOGLE_CLIENT_REGISTRATION.getRegistrationId(),
+				authentication, this.request);
+		verify(this.authorizedClientRepository).saveAuthorizedClient(refreshedAuthorizedClient, authentication,
+				this.request, this.response);
+		verify(this.refreshTokenAccessTokenResponseClient).getTokenResponse(refreshTokenGrantRequestCaptor.capture());
+		verify(this.jwtDecoder).decode(jwt.getTokenValue());
+		verify(this.oidcUserService).loadUser(userRequestCaptor.capture());
+		verify(this.securityContextRepository).saveContext(securityContextCaptor.capture(), eq(this.request),
+				eq(this.response));
+		verifyNoMoreInteractions(this.authorizedClientRepository, this.jwtDecoder, this.oidcUserService,
+				this.securityContextRepository);
+
+		OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = refreshTokenGrantRequestCaptor.getValue();
+		assertThat(refreshTokenGrantRequest.getClientRegistration())
+			.isEqualTo(authorizedClient.getClientRegistration());
+		assertThat(refreshTokenGrantRequest.getRefreshToken()).isEqualTo(authorizedClient.getRefreshToken());
+		assertThat(refreshTokenGrantRequest.getAccessToken()).isEqualTo(authorizedClient.getAccessToken());
+
+		OidcUserRequest userRequest = userRequestCaptor.getValue();
+		assertThat(userRequest.getClientRegistration()).isEqualTo(GOOGLE_CLIENT_REGISTRATION);
+		assertThat(userRequest.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken());
+		assertThat(userRequest.getIdToken().getTokenValue()).isEqualTo(jwt.getTokenValue());
+
+		SecurityContext refreshedSecurityContext = securityContextCaptor.getValue();
+		assertThat(refreshedSecurityContext).isNotNull();
+		assertThat(refreshedSecurityContext).isNotSameAs(securityContext);
+		assertThat(refreshedSecurityContext).isSameAs(SecurityContextHolder.getContext());
+		assertThat(refreshedSecurityContext.getAuthentication()).isInstanceOf(OAuth2AuthenticationToken.class);
+		assertThat(refreshedSecurityContext.getAuthentication()).isNotSameAs(authentication);
+		assertThat(refreshedSecurityContext.getAuthentication().getPrincipal()).isInstanceOf(OidcUser.class);
+		assertThat(refreshedSecurityContext.getAuthentication().getPrincipal())
+			.isNotSameAs(authentication.getPrincipal());
+	}
+
+	private OAuth2AuthorizedClient createAuthorizedClient() {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(30, ChronoUnit.SECONDS);
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN_VALUE,
+				issuedAt, expiresAt, Set.of(OidcScopes.OPENID));
+		OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(REFRESH_TOKEN_VALUE, issuedAt);
+
+		return new OAuth2AuthorizedClient(GOOGLE_CLIENT_REGISTRATION, SUBJECT, accessToken, refreshToken);
+	}
+
+	private OAuth2AccessTokenResponse createAccessTokenResponse(String... scope) {
+		Set<String> scopes = Set.of(scope);
+		Map<String, Object> additionalParameters = new HashMap<>();
+		if (scopes.contains(OidcScopes.OPENID)) {
+			additionalParameters.put(OidcParameterNames.ID_TOKEN, ID_TOKEN_VALUE);
+		}
+
+		return OAuth2AccessTokenResponse.withToken(ACCESS_TOKEN_VALUE)
+			.tokenType(OAuth2AccessToken.TokenType.BEARER)
+			.scopes(scopes)
+			.refreshToken(REFRESH_TOKEN_VALUE)
+			.expiresIn(60L)
+			.additionalParameters(additionalParameters)
+			.build();
+	}
+
+	private Jwt createJwt() {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
+		return TestJwts.jwt()
+			.subject(SUBJECT)
+			.tokenValue(ID_TOKEN_VALUE)
+			.issuedAt(issuedAt)
+			.expiresAt(expiresAt)
+			.build();
+	}
+
+	private OidcUser createOidcUser() {
+		Map<String, Object> claims = new HashMap<>();
+		claims.put(IdTokenClaimNames.SUB, SUBJECT);
+		claims.put(IdTokenClaimNames.ISS, "issuer");
+		claims.put(IdTokenClaimNames.AUD, List.of("audience1", "audience2"));
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
+		OidcIdToken idToken = new OidcIdToken(ID_TOKEN_VALUE, issuedAt, expiresAt, claims);
+
+		return new DefaultOidcUser(AuthorityUtils.createAuthorityList("OIDC_USER"), idToken);
+	}
+
+	private OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration) {
+		OidcUser oidcUser = createOidcUser();
+		return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
+				clientRegistration.getRegistrationId());
+	}
+
+	@Configuration
+	@EnableWebSecurity
+	static class OAuth2LoginWithOAuth2ClientConfig {
+
+		@Bean
+		SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.authorizeHttpRequests((authorize) -> authorize
+					.anyRequest().authenticated()
+				)
+				.securityContext((securityContext) -> securityContext
+					.securityContextRepository(this.securityContextRepository())
+				)
+				.oauth2Login(Customizer.withDefaults())
+				.oauth2Client(Customizer.withDefaults());
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		SecurityContextRepository securityContextRepository() {
+			return mock(SecurityContextRepository.class);
+		}
+
+		@Bean
+		ClientRegistrationRepository clientRegistrationRepository() {
+			return mock(ClientRegistrationRepository.class);
+		}
+
+		@Bean
+		OAuth2AuthorizedClientRepository authorizedClientRepository() {
+			return mock(OAuth2AuthorizedClientRepository.class);
+		}
+
+		@Bean
+		@SuppressWarnings("unchecked")
+		OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenAccessTokenResponseClient() {
+			return mock(OAuth2AccessTokenResponseClient.class);
+		}
+
+		@Bean
+		JwtDecoder jwtDecoder() {
+			return mock(JwtDecoder.class);
+		}
+
+		@Bean
+		JwtDecoderFactory<ClientRegistration> jwtDecoderFactory() {
+			return (clientRegistration) -> jwtDecoder();
+		}
+
+		@Bean
+		OidcUserService oidcUserService() {
+			return mock(OidcUserService.class);
+		}
+
+	}
+
+	private static final class CustomOAuth2AuthenticationToken extends OAuth2AuthenticationToken {
+
+		CustomOAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
+				String authorizedClientRegistrationId) {
+			super(principal, authorities, authorizedClientRegistrationId);
+		}
+
+	}
+
+}

+ 135 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListenerTests.java

@@ -0,0 +1,135 @@
+/*
+ * 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.config.annotation.web.configurers.oauth2.client;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.oidc.authentication.event.OidcUserRefreshedEvent;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link OidcUserRefreshedEventListener}.
+ *
+ * @author Steve Riesenberg
+ */
+public class OidcUserRefreshedEventListenerTests {
+
+	private OidcUserRefreshedEventListener eventListener;
+
+	private SecurityContextRepository securityContextRepository;
+
+	private MockHttpServletRequest request;
+
+	private MockHttpServletResponse response;
+
+	@BeforeEach
+	public void setUp() {
+		this.securityContextRepository = mock(SecurityContextRepository.class);
+		this.eventListener = new OidcUserRefreshedEventListener();
+		this.eventListener.setSecurityContextRepository(this.securityContextRepository);
+
+		this.request = new MockHttpServletRequest("GET", "");
+		this.request.setServletPath("/");
+		this.response = new MockHttpServletResponse();
+	}
+
+	@AfterEach
+	public void cleanUp() {
+		SecurityContextHolder.clearContext();
+		RequestContextHolder.resetRequestAttributes();
+	}
+
+	@Test
+	public void setSecurityContextHolderStrategyWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setSecurityContextHolderStrategy(null))
+			.withMessage("securityContextHolderStrategy cannot be null");
+	}
+
+	@Test
+	public void setSecurityContextRepositoryWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setSecurityContextRepository(null))
+			.withMessage("securityContextRepository cannot be null");
+	}
+
+	@Test
+	public void onApplicationEventWhenRequestAttributesSetThenSecurityContextSaved() {
+		RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(this.request, this.response));
+
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.oidcAccessTokenResponse()
+			.build();
+		OidcUser oldOidcUser = TestOidcUsers.create();
+		OidcUser newOidcUser = TestOidcUsers.create();
+		OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(newOidcUser,
+				newOidcUser.getAuthorities(), "test");
+		OidcUserRefreshedEvent event = new OidcUserRefreshedEvent(accessTokenResponse, oldOidcUser, newOidcUser,
+				authentication);
+		this.eventListener.onApplicationEvent(event);
+
+		ArgumentCaptor<SecurityContext> securityContextCaptor = ArgumentCaptor.forClass(SecurityContext.class);
+		verify(this.securityContextRepository).saveContext(securityContextCaptor.capture(), eq(this.request),
+				eq(this.response));
+		verifyNoMoreInteractions(this.securityContextRepository);
+
+		SecurityContext securityContext = securityContextCaptor.getValue();
+		assertThat(securityContext).isNotNull();
+		assertThat(securityContext).isSameAs(SecurityContextHolder.getContext());
+		assertThat(securityContext.getAuthentication()).isSameAs(authentication);
+	}
+
+	@Test
+	public void onApplicationEventWhenRequestAttributesNotSetThenSecurityContextNotSaved() {
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.oidcAccessTokenResponse()
+			.build();
+		OidcUser oldOidcUser = TestOidcUsers.create();
+		OidcUser newOidcUser = TestOidcUsers.create();
+		OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(newOidcUser,
+				newOidcUser.getAuthorities(), "test");
+		OidcUserRefreshedEvent event = new OidcUserRefreshedEvent(accessTokenResponse, oldOidcUser, newOidcUser,
+				authentication);
+		OidcUserRefreshedEventListener eventListener = new OidcUserRefreshedEventListener();
+		eventListener.setSecurityContextRepository(this.securityContextRepository);
+		eventListener.onApplicationEvent(event);
+		verifyNoInteractions(this.securityContextRepository);
+
+		SecurityContext securityContext = SecurityContextHolder.getContext();
+		assertThat(securityContext).isNotNull();
+		assertThat(securityContext.getAuthentication()).isSameAs(authentication);
+	}
+
+}

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * 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.
@@ -387,6 +387,7 @@ public final class OAuth2AuthorizedClientProviderBuilder {
 		 * refreshed.
 		 * @param eventPublisher the {@link ApplicationEventPublisher}
 		 * @return the {@link RefreshTokenGrantBuilder}
+		 * @since 6.5
 		 */
 		public RefreshTokenGrantBuilder eventPublisher(ApplicationEventPublisher eventPublisher) {
 			this.eventPublisher = eventPublisher;

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * 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.
@@ -30,7 +30,7 @@ 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.client.event.OAuth2AuthorizedClientRefreshedEvent;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
 import org.springframework.security.oauth2.core.OAuth2Token;
@@ -51,7 +51,7 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider
 
 	private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
 
-	private ApplicationEventPublisher eventPublisher;
+	private ApplicationEventPublisher applicationEventPublisher;
 
 	private Duration clockSkew = Duration.ofSeconds(60);
 
@@ -98,16 +98,17 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider
 				authorizedClient.getRefreshToken(), scopes);
 		OAuth2AccessTokenResponse tokenResponse = getTokenResponse(authorizedClient, refreshTokenGrantRequest);
 
-		OAuth2AuthorizedClient updatedOAuth2AuthorizedClient = new OAuth2AuthorizedClient(
+		OAuth2AuthorizedClient refreshedAuthorizedClient = new OAuth2AuthorizedClient(
 				authorizedClient.getClientRegistration(), context.getPrincipal().getName(),
 				tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
 
-		if (this.eventPublisher != null) {
-			this.eventPublisher
-				.publishEvent(new OAuth2TokenRefreshedEvent(this, updatedOAuth2AuthorizedClient, tokenResponse));
+		if (this.applicationEventPublisher != null) {
+			OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+					tokenResponse, refreshedAuthorizedClient);
+			this.applicationEventPublisher.publishEvent(authorizedClientRefreshedEvent);
 		}
 
-		return updatedOAuth2AuthorizedClient;
+		return refreshedAuthorizedClient;
 	}
 
 	private OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient,
@@ -166,7 +167,8 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider
 
 	@Override
 	public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
-		this.eventPublisher = applicationEventPublisher;
+		Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
+		this.applicationEventPublisher = applicationEventPublisher;
 	}
 
 }

+ 69 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2AuthorizedClientRefreshedEvent.java

@@ -0,0 +1,69 @@
+/*
+ * 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 java.io.Serial;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.util.Assert;
+
+/**
+ * An event that is published when an {@link OAuth2AuthorizedClient} is refreshed as a
+ * result of using a {@code refresh_token} to obtain an OAuth 2.0 Access Token Response.
+ *
+ * @author Steve Riesenberg
+ * @since 6.5
+ */
+public final class OAuth2AuthorizedClientRefreshedEvent extends ApplicationEvent {
+
+	@Serial
+	private static final long serialVersionUID = -2178028089321556476L;
+
+	private final OAuth2AuthorizedClient authorizedClient;
+
+	/**
+	 * Creates a new instance with the provided parameters.
+	 * @param accessTokenResponse the {@link OAuth2AccessTokenResponse} that triggered the
+	 * event
+	 * @param authorizedClient the refreshed {@link OAuth2AuthorizedClient}
+	 */
+	public OAuth2AuthorizedClientRefreshedEvent(OAuth2AccessTokenResponse accessTokenResponse,
+			OAuth2AuthorizedClient authorizedClient) {
+		super(accessTokenResponse);
+		Assert.notNull(authorizedClient, "authorizedClient cannot be null");
+		this.authorizedClient = authorizedClient;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AccessTokenResponse} that triggered the event.
+	 * @return the access token response
+	 */
+	public OAuth2AccessTokenResponse getAccessTokenResponse() {
+		return (OAuth2AccessTokenResponse) this.getSource();
+	}
+
+	/**
+	 * Returns the refreshed {@link OAuth2AuthorizedClient}.
+	 * @return the authorized client
+	 */
+	public OAuth2AuthorizedClient getAuthorizedClient() {
+		return this.authorizedClient;
+	}
+
+}

+ 0 - 47
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/event/OAuth2TokenRefreshedEvent.java

@@ -1,47 +0,0 @@
-/*
- * 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;
-	}
-
-}

+ 219 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java

@@ -0,0 +1,219 @@
+/*
+ * 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.util.Collection;
+import java.util.Map;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
+import org.springframework.context.ApplicationListener;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
+import org.springframework.security.oauth2.client.event.OAuth2AuthorizedClientRefreshedEvent;
+import org.springframework.security.oauth2.client.oidc.authentication.event.OidcUserRefreshedEvent;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+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.OidcScopes;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+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;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link ApplicationListener} that listens for events of type
+ * {@link OAuth2AuthorizedClientRefreshedEvent} and publishes an event of type
+ * {@link OidcUserRefreshedEvent} in order to refresh an {@link OidcUser}.
+ *
+ * @author Steve Riesenberg
+ * @since 6.5
+ * @see org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider
+ * @see OAuth2AuthorizedClientRefreshedEvent
+ * @see OidcUserRefreshedEvent
+ */
+public final class OidcAuthorizedClientRefreshedEventListener
+		implements ApplicationEventPublisherAware, ApplicationListener<OAuth2AuthorizedClientRefreshedEvent> {
+
+	private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
+
+	private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
+
+	private OAuth2UserService<OidcUserRequest, OidcUser> userService = new OidcUserService();
+
+	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
+
+	private GrantedAuthoritiesMapper authoritiesMapper = (authorities) -> authorities;
+
+	private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
+		.getContextHolderStrategy();
+
+	private ApplicationEventPublisher applicationEventPublisher;
+
+	@Override
+	public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
+		if (this.applicationEventPublisher == null) {
+			return;
+		}
+
+		// The response must contain the openid scope
+		OAuth2AccessTokenResponse accessTokenResponse = event.getAccessTokenResponse();
+		if (!accessTokenResponse.getAccessToken().getScopes().contains(OidcScopes.OPENID)) {
+			return;
+		}
+
+		// The response must contain an id_token
+		Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
+		if (!StringUtils.hasText((String) additionalParameters.get(OidcParameterNames.ID_TOKEN))) {
+			return;
+		}
+
+		// The current authentication must be an OAuth2AuthenticationToken
+		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
+		if (!(authentication instanceof OAuth2AuthenticationToken authenticationToken)
+				|| authenticationToken.getClass() != OAuth2AuthenticationToken.class) {
+			// This event listener only handles the default authentication result. If the
+			// application customizes the authentication result, then a custom event
+			// handler should be provided.
+			return;
+		}
+
+		// The current principal must be an OidcUser
+		if (!(authenticationToken.getPrincipal() instanceof OidcUser existingOidcUser)) {
+			return;
+		}
+
+		// The registrationId must match the one used to log in
+		ClientRegistration clientRegistration = event.getAuthorizedClient().getClientRegistration();
+		if (!authenticationToken.getAuthorizedClientRegistrationId().equals(clientRegistration.getRegistrationId())) {
+			return;
+		}
+
+		// Refresh the OidcUser and send a user refreshed event
+		OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
+		validateNonce(existingOidcUser, idToken);
+		OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(),
+				idToken, additionalParameters);
+		OidcUser oidcUser = this.userService.loadUser(userRequest);
+		Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
+			.mapAuthorities(oidcUser.getAuthorities());
+		OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(oidcUser, mappedAuthorities,
+				clientRegistration.getRegistrationId());
+		authenticationResult.setDetails(authenticationToken.getDetails());
+		OidcUserRefreshedEvent oidcUserRefreshedEvent = new OidcUserRefreshedEvent(accessTokenResponse,
+				existingOidcUser, oidcUser, authenticationResult);
+		this.applicationEventPublisher.publishEvent(oidcUserRefreshedEvent);
+	}
+
+	/**
+	 * Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
+	 * the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
+	 */
+	public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
+		Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
+		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 void setJwtDecoderFactory(JwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
+		Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
+		this.jwtDecoderFactory = jwtDecoderFactory;
+	}
+
+	/**
+	 * Sets the {@link OAuth2UserService} used for obtaining the user attributes of the
+	 * End-User from the UserInfo Endpoint.
+	 * @param userService the service used for obtaining the user attributes of the
+	 * End-User from the UserInfo Endpoint
+	 */
+	public void setUserService(OAuth2UserService<OidcUserRequest, OidcUser> userService) {
+		Assert.notNull(userService, "userService cannot be null");
+		this.userService = userService;
+	}
+
+	/**
+	 * Sets the {@link GrantedAuthoritiesMapper} used for mapping
+	 * {@link OidcUser#getAuthorities()}} to a new set of authorities which will be
+	 * associated to the {@link OAuth2LoginAuthenticationToken}.
+	 * @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
+	 * user's authorities
+	 */
+	public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
+		Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
+		this.authoritiesMapper = authoritiesMapper;
+	}
+
+	/**
+	 * Sets the {@link ApplicationEventPublisher} to be used.
+	 * @param applicationEventPublisher event publisher to be used
+	 */
+	@Override
+	public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+		Assert.notNull(applicationEventPublisher, "applicationEventPublisher cannot be null");
+		this.applicationEventPublisher = applicationEventPublisher;
+	}
+
+	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<String, Object> 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);
+		}
+	}
+
+	private void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {
+		if (!StringUtils.hasText(idToken.getNonce())) {
+			return;
+		}
+
+		if (!idToken.getNonce().equals(existingOidcUser.getNonce())) {
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
+			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+		}
+	}
+
+}

+ 0 - 139
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandler.java

@@ -1,139 +0,0 @@
-/*
- * 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.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.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;
-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<OAuth2TokenRefreshedEvent> {
-
-	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
-		.getContextHolderStrategy();
-
-	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
-
-	@Override
-	public void onApplicationEvent(OAuth2TokenRefreshedEvent event) {
-		OAuth2AuthorizedClient authorizedClient = event.getAuthorizedClient();
-
-		if (!authorizedClient.getClientRegistration().getScopes().contains(OidcScopes.OPENID)) {
-			return;
-		}
-
-		Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
-		if (!(authentication instanceof OAuth2AuthenticationToken oauth2Authentication)) {
-			return;
-		}
-		if (!(authentication.getPrincipal() instanceof DefaultOidcUser defaultOidcUser)) {
-			return;
-		}
-
-		OAuth2AccessTokenResponse accessTokenResponse = event.getAccessTokenResponse();
-
-		String idToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
-		if (idToken == null || idToken.isBlank()) {
-			OAuth2Error missingIdTokenError = new OAuth2Error(MISSING_ID_TOKEN_ERROR_CODE,
-					"ID token is missing in the token response", null);
-			throw new OAuth2AuthenticationException(missingIdTokenError, missingIdTokenError.toString());
-		}
-
-		ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
-		OidcIdToken refreshedOidcToken = createOidcToken(clientRegistration, accessTokenResponse);
-		updateSecurityContext(oauth2Authentication, defaultOidcUser, refreshedOidcToken);
-	}
-
-	/**
-	 * 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<ClientRegistration> jwtDecoderFactory) {
-		Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
-		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);
-		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<String, Object> 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);
-		}
-	}
-
-}

+ 98 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/event/OidcUserRefreshedEvent.java

@@ -0,0 +1,98 @@
+/*
+ * 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.event;
+
+import java.io.Serial;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.util.Assert;
+
+/**
+ * An event that is published when an {@link OidcUser} is refreshed as a result of using a
+ * {@code refresh_token} to obtain an OAuth 2.0 Access Token Response that contains an
+ * {@code id_token}.
+ *
+ * @author Steve Riesenberg
+ * @since 6.5
+ * @see org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider
+ */
+public final class OidcUserRefreshedEvent extends ApplicationEvent {
+
+	@Serial
+	private static final long serialVersionUID = 2657442604286019694L;
+
+	private final OidcUser oldOidcUser;
+
+	private final OidcUser newOidcUser;
+
+	private final Authentication authentication;
+
+	/**
+	 * Creates a new instance with the provided parameters.
+	 * @param accessTokenResponse the {@link OAuth2AccessTokenResponse} that triggered the
+	 * event
+	 * @param oldOidcUser the original {@link OidcUser}
+	 * @param newOidcUser the refreshed {@link OidcUser}
+	 * @param authentication the authentication result
+	 */
+	public OidcUserRefreshedEvent(OAuth2AccessTokenResponse accessTokenResponse, OidcUser oldOidcUser,
+			OidcUser newOidcUser, Authentication authentication) {
+		super(accessTokenResponse);
+		Assert.notNull(oldOidcUser, "oldOidcUser cannot be null");
+		Assert.notNull(newOidcUser, "newOidcUser cannot be null");
+		Assert.notNull(authentication, "authentication cannot be null");
+		this.oldOidcUser = oldOidcUser;
+		this.newOidcUser = newOidcUser;
+		this.authentication = authentication;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AccessTokenResponse} that triggered the event.
+	 * @return the access token response
+	 */
+	public OAuth2AccessTokenResponse getAccessTokenResponse() {
+		return (OAuth2AccessTokenResponse) this.getSource();
+	}
+
+	/**
+	 * Returns the original {@link OidcUser}.
+	 * @return the original user
+	 */
+	public OidcUser getOldOidcUser() {
+		return this.oldOidcUser;
+	}
+
+	/**
+	 * Returns the refreshed {@link OidcUser}.
+	 * @return the refreshed user
+	 */
+	public OidcUser getNewOidcUser() {
+		return this.newOidcUser;
+	}
+
+	/**
+	 * Returns the authentication result.
+	 * @return the authentication result
+	 */
+	public Authentication getAuthentication() {
+		return this.authentication;
+	}
+
+}

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

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -30,7 +30,7 @@ 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.event.OAuth2AuthorizedClientRefreshedEvent;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -297,7 +297,7 @@ public class RefreshTokenOAuth2AuthorizedClientProviderTests {
 
 		@Override
 		public void publishEvent(Object event) {
-			if (OAuth2TokenRefreshedEvent.class.isAssignableFrom(event.getClass())) {
+			if (OAuth2AuthorizedClientRefreshedEvent.class.isAssignableFrom(event.getClass())) {
 				this.flag = true;
 			}
 		}

+ 420 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListenerTests.java

@@ -0,0 +1,420 @@
+/*
+ * 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.time.temporal.ChronoUnit;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+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.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.event.OAuth2AuthorizedClientRefreshedEvent;
+import org.springframework.security.oauth2.client.oidc.authentication.event.OidcUserRefreshedEvent;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.endpoint.TestOAuth2AccessTokenResponses;
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
+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;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.oauth2.jwt.TestJwts;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests for {@link OidcAuthorizedClientRefreshedEventListener}.
+ *
+ * @author Steve Riesenberg
+ */
+public class OidcAuthorizedClientRefreshedEventListenerTests {
+
+	private static final String SUBJECT = "surfer-dude";
+
+	private static final String ACCESS_TOKEN_VALUE = "hang-ten";
+
+	private static final String REFRESH_TOKEN_VALUE = "surfs-up";
+
+	private static final String ID_TOKEN_VALUE = "beach-break";
+
+	private OidcAuthorizedClientRefreshedEventListener eventListener;
+
+	private SecurityContextHolderStrategy securityContextHolderStrategy;
+
+	private JwtDecoder jwtDecoder;
+
+	private OidcUserService userService;
+
+	private ApplicationEventPublisher applicationEventPublisher;
+
+	private ClientRegistration clientRegistration;
+
+	private OAuth2AuthorizedClient authorizedClient;
+
+	private OAuth2AccessTokenResponse accessTokenResponse;
+
+	private Jwt jwt;
+
+	private OidcUser oidcUser;
+
+	@BeforeEach
+	public void setUp() {
+		this.jwtDecoder = mock(JwtDecoder.class);
+		this.userService = mock(OidcUserService.class);
+		this.securityContextHolderStrategy = mock(SecurityContextHolderStrategy.class);
+		this.applicationEventPublisher = mock(ApplicationEventPublisher.class);
+
+		this.eventListener = new OidcAuthorizedClientRefreshedEventListener();
+		this.eventListener.setUserService(this.userService);
+		this.eventListener.setJwtDecoderFactory((clientRegistration) -> this.jwtDecoder);
+		this.eventListener.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
+		this.eventListener.setApplicationEventPublisher(this.applicationEventPublisher);
+
+		this.clientRegistration = TestClientRegistrations.clientRegistration().scope(OidcScopes.OPENID).build();
+		this.authorizedClient = createAuthorizedClient(this.clientRegistration);
+		this.accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
+		this.jwt = createJwt();
+		this.oidcUser = createOidcUser();
+	}
+
+	@Test
+	public void setSecurityContextHolderStrategyWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setSecurityContextHolderStrategy(null))
+			.withMessage("securityContextHolderStrategy cannot be null");
+	}
+
+	@Test
+	public void setJwtDecoderFactoryWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setJwtDecoderFactory(null))
+			.withMessage("jwtDecoderFactory cannot be null");
+	}
+
+	@Test
+	public void setUserServiceWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setUserService(null))
+			.withMessage("userService cannot be null");
+	}
+
+	@Test
+	public void setAuthoritiesMapperWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setAuthoritiesMapper(null))
+			.withMessage("authoritiesMapper cannot be null");
+	}
+
+	@Test
+	public void setApplicationEventPublisherWhenNullThenThrowsIllegalArgumentException() {
+		assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setApplicationEventPublisher(null))
+			.withMessage("applicationEventPublisher cannot be null");
+	}
+
+	@Test
+	public void onApplicationEventWhenAccessTokenResponseMissingIdTokenThenOidcUserRefreshedEventNotPublished() {
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.accessTokenResponse()
+			.scopes(Set.of(OidcScopes.OPENID))
+			.build();
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+		verifyNoInteractions(this.securityContextHolderStrategy, this.jwtDecoder, this.userService,
+				this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenAccessTokenResponseMissingOpenidScopeThenOidcUserRefreshedEventNotPublished() {
+		OAuth2AccessTokenResponse accessTokenResponse = TestOAuth2AccessTokenResponses.oidcAccessTokenResponse()
+			.scopes(Set.of())
+			.build();
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+		verifyNoInteractions(this.securityContextHolderStrategy, this.jwtDecoder, this.userService,
+				this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenAuthenticationIsNotOAuth2ThenOidcUserRefreshedEventNotPublished() {
+		TestingAuthenticationToken authentication = new TestingAuthenticationToken(SUBJECT, null);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		verify(this.securityContextHolderStrategy).getContext();
+		verifyNoMoreInteractions(this.securityContextHolderStrategy);
+		verifyNoInteractions(this.jwtDecoder, this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenAuthenticationIsCustomThenOidcUserRefreshedEventNotPublished() {
+		OAuth2AuthenticationToken authentication = new CustomOAuth2AuthenticationToken(this.oidcUser,
+				this.oidcUser.getAuthorities(), this.clientRegistration.getRegistrationId());
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		verify(this.securityContextHolderStrategy).getContext();
+		verifyNoMoreInteractions(this.securityContextHolderStrategy);
+		verifyNoInteractions(this.jwtDecoder, this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenPrincipalIsOAuth2UserThenOidcUserRefreshedEventNotPublished() {
+		Map<String, Object> attributes = Map.of(StandardClaimNames.SUB, SUBJECT);
+		OAuth2User oauth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("OAUTH2_USER"), attributes,
+				StandardClaimNames.SUB);
+		OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oauth2User,
+				oauth2User.getAuthorities(), this.clientRegistration.getRegistrationId());
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		verify(this.securityContextHolderStrategy).getContext();
+		verifyNoMoreInteractions(this.securityContextHolderStrategy);
+		verifyNoInteractions(this.jwtDecoder, this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenClientRegistrationIdDoesNotMatchThenOidcUserRefreshedEventNotPublished() {
+		ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
+			.registrationId("test")
+			.build();
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(clientRegistration);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		verify(this.securityContextHolderStrategy).getContext();
+		verifyNoMoreInteractions(this.securityContextHolderStrategy);
+		verifyNoInteractions(this.jwtDecoder, this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenAccessTokenResponseIncludesIdTokenThenPublishOidcUserRefreshedEvent() {
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+		given(this.jwtDecoder.decode(anyString())).willReturn(this.jwt);
+		given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		ArgumentCaptor<OidcUserRequest> userRequestCaptor = ArgumentCaptor.forClass(OidcUserRequest.class);
+		ArgumentCaptor<OidcUserRefreshedEvent> userRefreshedEventCaptor = ArgumentCaptor
+			.forClass(OidcUserRefreshedEvent.class);
+		verify(this.securityContextHolderStrategy).getContext();
+		verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
+		verify(this.userService).loadUser(userRequestCaptor.capture());
+		verify(this.applicationEventPublisher).publishEvent(userRefreshedEventCaptor.capture());
+		verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder, this.userService,
+				this.applicationEventPublisher);
+
+		OidcUserRequest userRequest = userRequestCaptor.getValue();
+		assertThat(userRequest.getClientRegistration()).isSameAs(this.clientRegistration);
+		assertThat(userRequest.getAccessToken()).isSameAs(this.accessTokenResponse.getAccessToken());
+		assertThat(userRequest.getIdToken().getTokenValue()).isEqualTo(this.jwt.getTokenValue());
+
+		OidcUserRefreshedEvent userRefreshedEvent = userRefreshedEventCaptor.getValue();
+		assertThat(userRefreshedEvent.getAccessTokenResponse()).isSameAs(this.accessTokenResponse);
+		assertThat(userRefreshedEvent.getOldOidcUser()).isSameAs(authentication.getPrincipal());
+		assertThat(userRefreshedEvent.getNewOidcUser()).isSameAs(this.oidcUser);
+		assertThat(userRefreshedEvent.getAuthentication()).isNotSameAs(authentication);
+		assertThat(userRefreshedEvent.getAuthentication()).isInstanceOf(OAuth2AuthenticationToken.class);
+
+		OAuth2AuthenticationToken authenticationResult = (OAuth2AuthenticationToken) userRefreshedEvent
+			.getAuthentication();
+		assertThat(authenticationResult.getPrincipal()).isEqualTo(this.oidcUser);
+		assertThat(authenticationResult.getAuthorities()).containsExactlyElementsOf(this.oidcUser.getAuthorities());
+		assertThat(authenticationResult.getAuthorizedClientRegistrationId())
+			.isEqualTo(this.clientRegistration.getRegistrationId());
+	}
+
+	@Test
+	public void onApplicationEventWhenIdTokenNonceDoesNotMatchThenThrowsOAuth2AuthenticationException() {
+		Jwt jwt = TestJwts.jwt().claim(IdTokenClaimNames.NONCE, "invalid").build();
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+		given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		assertThatExceptionOfType(OAuth2AuthenticationException.class)
+			.isThrownBy(() -> this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent))
+			.extracting(OAuth2AuthenticationException::getError)
+			.extracting(OAuth2Error::getErrorCode)
+			.isEqualTo("invalid_nonce");
+		verify(this.securityContextHolderStrategy).getContext();
+		verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
+		verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
+		verifyNoInteractions(this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenInvalidIdTokenThenThrowsOAuth2AuthenticationException() {
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+		given(this.jwtDecoder.decode(anyString())).willThrow(new JwtException("Invalid token"));
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		assertThatExceptionOfType(OAuth2AuthenticationException.class)
+			.isThrownBy(() -> this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent))
+			.extracting(OAuth2AuthenticationException::getError)
+			.extracting(OAuth2Error::getErrorCode)
+			.isEqualTo("invalid_id_token");
+		verify(this.securityContextHolderStrategy).getContext();
+		verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
+		verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
+		verifyNoInteractions(this.userService, this.applicationEventPublisher);
+	}
+
+	@Test
+	public void onApplicationEventWhenCustomAuthoritiesMapperSetThenUsed() {
+		OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
+		SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
+		given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
+		given(this.jwtDecoder.decode(anyString())).willReturn(this.jwt);
+		given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
+
+		GrantedAuthoritiesMapper grantedAuthoritiesMapper = mock(GrantedAuthoritiesMapper.class);
+		this.eventListener.setAuthoritiesMapper(grantedAuthoritiesMapper);
+
+		OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
+				this.accessTokenResponse, this.authorizedClient);
+		this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
+
+		verify(grantedAuthoritiesMapper).mapAuthorities(this.oidcUser.getAuthorities());
+		verifyNoMoreInteractions(grantedAuthoritiesMapper);
+	}
+
+	private static OAuth2AuthorizedClient createAuthorizedClient(ClientRegistration clientRegistration) {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(30, ChronoUnit.SECONDS);
+		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, ACCESS_TOKEN_VALUE,
+				issuedAt, expiresAt, clientRegistration.getScopes());
+
+		return new OAuth2AuthorizedClient(clientRegistration, SUBJECT, accessToken);
+	}
+
+	private static OAuth2AccessTokenResponse createAccessTokenResponse(String... scope) {
+		Set<String> scopes = Set.of(scope);
+		Map<String, Object> additionalParameters = new HashMap<>();
+		if (scopes.contains(OidcScopes.OPENID)) {
+			additionalParameters.put(OidcParameterNames.ID_TOKEN, ID_TOKEN_VALUE);
+		}
+
+		return OAuth2AccessTokenResponse.withToken(ACCESS_TOKEN_VALUE)
+			.tokenType(OAuth2AccessToken.TokenType.BEARER)
+			.scopes(scopes)
+			.refreshToken(REFRESH_TOKEN_VALUE)
+			.expiresIn(60L)
+			.additionalParameters(additionalParameters)
+			.build();
+	}
+
+	private static Jwt createJwt() {
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
+		return TestJwts.jwt()
+			.subject(SUBJECT)
+			.tokenValue(ID_TOKEN_VALUE)
+			.issuedAt(issuedAt)
+			.expiresAt(expiresAt)
+			.claim(OidcParameterNames.NONCE, "nonce")
+			.build();
+	}
+
+	private static OidcUser createOidcUser() {
+		Map<String, Object> claims = new HashMap<>();
+		claims.put(IdTokenClaimNames.SUB, SUBJECT);
+		claims.put(IdTokenClaimNames.ISS, "issuer");
+		claims.put(IdTokenClaimNames.AUD, List.of("audience1", "audience2"));
+		claims.put(IdTokenClaimNames.NONCE, "nonce");
+		Instant issuedAt = Instant.now();
+		Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
+		OidcIdToken idToken = new OidcIdToken(ID_TOKEN_VALUE, issuedAt, expiresAt, claims);
+
+		return new DefaultOidcUser(AuthorityUtils.createAuthorityList("OIDC_USER"), idToken);
+	}
+
+	private static OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration) {
+		OidcUser oidcUser = createOidcUser();
+		return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
+				clientRegistration.getRegistrationId());
+	}
+
+	private static final class CustomOAuth2AuthenticationToken extends OAuth2AuthenticationToken {
+
+		CustomOAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
+				String authorizedClientRegistrationId) {
+			super(principal, authorities, authorizedClientRegistrationId);
+		}
+
+	}
+
+}

+ 0 - 284
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/RefreshOidcIdTokenHandlerTests.java

@@ -1,284 +0,0 @@
-/*
- * 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<ClientRegistration> 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<String, Object> 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<OAuth2AuthenticationToken> 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)));
-	}
-
-}