mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 01:02:14 +00:00
Polish gh-16589
This commit is contained in:
parent
fc1469ad5e
commit
5bb5d0f6be
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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,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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user