Add additional validation when refreshing ID tokens

Issue gh-16589
This commit is contained in:
Steve Riesenberg 2025-03-25 12:34:36 -05:00
parent 5f98ce5ecc
commit 860f130bc4
No known key found for this signature in database
GPG Key ID: 3D0169B18AB8F0A9
3 changed files with 341 additions and 48 deletions

View File

@ -171,7 +171,8 @@ public class OidcUserRefreshedEventListenerConfigurationTests {
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
.willReturn(accessTokenResponse);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
createOidcUser());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
.principal(authentication)
@ -194,7 +195,8 @@ public class OidcUserRefreshedEventListenerConfigurationTests {
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
.willReturn(accessTokenResponse);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
createOidcUser());
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
.principal(authentication)
@ -297,7 +299,8 @@ public class OidcUserRefreshedEventListenerConfigurationTests {
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
.willReturn(accessTokenResponse);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GITHUB_CLIENT_REGISTRATION);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GITHUB_CLIENT_REGISTRATION,
createOidcUser());
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
SecurityContextHolder.setContext(securityContext);
@ -316,17 +319,17 @@ public class OidcUserRefreshedEventListenerConfigurationTests {
OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
Jwt jwt = createJwt();
OidcUser oidcUser = createOidcUser();
Jwt jwt = createJwt().build();
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);
given(this.oidcUserService.loadUser(any(OidcUserRequest.class))).willReturn(createOidcUser());
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
createOidcUser());
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
SecurityContextHolder.setContext(securityContext);
@ -405,31 +408,36 @@ public class OidcUserRefreshedEventListenerConfigurationTests {
.build();
}
private Jwt createJwt() {
private static Jwt.Builder createJwt() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
return TestJwts.jwt()
.issuer("https://surf.school")
.subject(SUBJECT)
.tokenValue(ID_TOKEN_VALUE)
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.build();
.audience(List.of("audience1", "audience2"));
}
private OidcUser createOidcUser() {
private static OidcUser createOidcUser() {
Instant issuedAt = Instant.now().minus(30, ChronoUnit.SECONDS);
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
Map<String, Object> claims = new HashMap<>();
claims.put(IdTokenClaimNames.ISS, "https://surf.school");
claims.put(IdTokenClaimNames.SUB, SUBJECT);
claims.put(IdTokenClaimNames.ISS, "issuer");
claims.put(IdTokenClaimNames.IAT, issuedAt);
claims.put(IdTokenClaimNames.EXP, expiresAt);
claims.put(IdTokenClaimNames.AUD, List.of("audience1", "audience2"));
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
claims.put(IdTokenClaimNames.AUTH_TIME, issuedAt);
claims.put(IdTokenClaimNames.NONCE, "nonce");
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();
private OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration,
OidcUser oidcUser) {
return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
clientRegistration.getRegistrationId());
}

View File

@ -16,8 +16,12 @@
package org.springframework.security.oauth2.client.oidc.authentication;
import java.time.Duration;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
@ -67,6 +71,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse";
private OAuth2UserService<OidcUserRequest, OidcUser> userService = new OidcUserService();
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
@ -78,6 +84,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
private ApplicationEventPublisher applicationEventPublisher;
private Duration clockSkew = Duration.ofSeconds(60);
@Override
public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
if (this.applicationEventPublisher == null) {
@ -119,7 +127,7 @@ public final class OidcAuthorizedClientRefreshedEventListener
// Refresh the OidcUser and send a user refreshed event
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
validateNonce(existingOidcUser, idToken);
validateIdToken(existingOidcUser, idToken);
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(),
idToken, additionalParameters);
OidcUser oidcUser = this.userService.loadUser(userRequest);
@ -187,6 +195,17 @@ public final class OidcAuthorizedClientRefreshedEventListener
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OidcIdToken#getIssuedAt() issuedAt} time. The default is 60 seconds.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
this.clockSkew = clockSkew;
}
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
OAuth2AccessTokenResponse accessTokenResponse) {
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
@ -205,13 +224,97 @@ public final class OidcAuthorizedClientRefreshedEventListener
}
}
private void validateIdToken(OidcUser existingOidcUser, OidcIdToken idToken) {
// OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
// If an ID Token is returned as a result of a token refresh request, the
// following requirements apply:
// its iss Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateIssuer(existingOidcUser, idToken);
// its sub Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateSubject(existingOidcUser, idToken);
// its iat Claim MUST represent the time that the new ID Token is issued,
validateIssuedAt(existingOidcUser, idToken);
// its aud Claim Value MUST be the same as in the ID Token issued when the
// original authentication occurred,
validateAudience(existingOidcUser, idToken);
// if the ID Token contains an auth_time Claim, its value MUST represent the time
// of the original authentication - not the time that the new ID token is issued,
validateAuthenticatedAt(existingOidcUser, idToken);
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
// the original authentication contained nonce; however, if it is present, its
// value MUST be the same as in the ID Token issued at the time of the original
// authentication,
validateNonce(existingOidcUser, idToken);
}
private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
if (!isValidAudience(existingOidcUser, idToken)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid audience",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
List<String> idTokenAudiences = idToken.getAudience();
Set<String> oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience());
if (idTokenAudiences.size() != oidcUserAudiences.size()) {
return false;
}
for (String audience : idTokenAudiences) {
if (!oidcUserAudiences.contains(audience)) {
return false;
}
}
return true;
}
private void validateAuthenticatedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
if (idToken.getAuthenticatedAt() == null) {
return;
}
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
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);
if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce",
REFRESH_TOKEN_RESPONSE_ERROR_URI);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.client.oidc.authentication;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
@ -80,6 +81,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/
public class OidcAuthorizedClientRefreshedEventListenerTests {
private static final String INVALID_ID_TOKEN_ERROR = "invalid_id_token";
private static final String INVALID_NONCE_ERROR = "invalid_nonce";
private static final String SUBJECT = "surfer-dude";
private static final String ACCESS_TOKEN_VALUE = "hang-ten";
@ -108,6 +113,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
private OidcUser oidcUser;
private OAuth2AuthenticationToken authentication;
@BeforeEach
public void setUp() {
this.jwtDecoder = mock(JwtDecoder.class);
@ -124,38 +131,72 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
this.clientRegistration = TestClientRegistrations.clientRegistration().scope(OidcScopes.OPENID).build();
this.authorizedClient = createAuthorizedClient(this.clientRegistration);
this.accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
this.jwt = createJwt();
this.jwt = createJwt().build();
this.oidcUser = createOidcUser();
this.authentication = createAuthenticationToken(this.clientRegistration, createOidcUser());
}
@Test
public void setSecurityContextHolderStrategyWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setSecurityContextHolderStrategy(null))
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setSecurityContextHolderStrategy(null))
.withMessage("securityContextHolderStrategy cannot be null");
// @formatter:on
}
@Test
public void setJwtDecoderFactoryWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setJwtDecoderFactory(null))
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setJwtDecoderFactory(null))
.withMessage("jwtDecoderFactory cannot be null");
// @formatter:on
}
@Test
public void setUserServiceWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setUserService(null))
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setUserService(null))
.withMessage("userService cannot be null");
// @formatter:on
}
@Test
public void setAuthoritiesMapperWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setAuthoritiesMapper(null))
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setAuthoritiesMapper(null))
.withMessage("authoritiesMapper cannot be null");
// @formatter:on
}
@Test
public void setApplicationEventPublisherWhenNullThenThrowsIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.eventListener.setApplicationEventPublisher(null))
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setApplicationEventPublisher(null))
.withMessage("applicationEventPublisher cannot be null");
// @formatter:on
}
@Test
public void setClockSkewWhenNullThenThrowsIllegalArgumentException() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setClockSkew(null))
.withMessage("clockSkew cannot be null");
// @formatter:on
}
@Test
public void setClockSkewWhenNegativeThenThrowsIllegalArgumentException() {
// @formatter:off
assertThatIllegalArgumentException()
.isThrownBy(() -> this.eventListener.setClockSkew(Duration.ofMillis(-1)))
.withMessage("clockSkew must be >= 0");
// @formatter:on
}
@Test
@ -237,7 +278,7 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration()
.registrationId("test")
.build();
OAuth2AuthenticationToken authentication = createAuthenticationToken(clientRegistration);
OAuth2AuthenticationToken authentication = createAuthenticationToken(clientRegistration, this.oidcUser);
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
@ -251,9 +292,8 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
}
@Test
public void onApplicationEventWhenAccessTokenResponseIncludesIdTokenThenPublishOidcUserRefreshedEvent() {
OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
public void onApplicationEventWhenAccessTokenResponseIncludesIdTokenThenOidcUserRefreshedEventPublished() {
SecurityContextImpl securityContext = new SecurityContextImpl(this.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);
@ -279,9 +319,10 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
OidcUserRefreshedEvent userRefreshedEvent = userRefreshedEventCaptor.getValue();
assertThat(userRefreshedEvent.getAccessTokenResponse()).isSameAs(this.accessTokenResponse);
assertThat(userRefreshedEvent.getOldOidcUser()).isSameAs(authentication.getPrincipal());
assertThat(userRefreshedEvent.getOldOidcUser()).isSameAs(this.authentication.getPrincipal());
assertThat(userRefreshedEvent.getNewOidcUser()).isSameAs(this.oidcUser);
assertThat(userRefreshedEvent.getAuthentication()).isNotSameAs(authentication);
assertThat(userRefreshedEvent.getOldOidcUser()).isNotSameAs(userRefreshedEvent.getNewOidcUser());
assertThat(userRefreshedEvent.getAuthentication()).isNotSameAs(this.authentication);
assertThat(userRefreshedEvent.getAuthentication()).isInstanceOf(OAuth2AuthenticationToken.class);
OAuth2AuthenticationToken authenticationResult = (OAuth2AuthenticationToken) userRefreshedEvent
@ -293,10 +334,9 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
}
@Test
public void onApplicationEventWhenIdTokenNonceDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Jwt jwt = TestJwts.jwt().claim(IdTokenClaimNames.NONCE, "invalid").build();
OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
public void onApplicationEventWhenIdTokenIssuerDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Jwt jwt = createJwt().issuer("https://invalid.url").build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
@ -304,19 +344,159 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
this.accessTokenResponse, this.authorizedClient);
assertThatExceptionOfType(OAuth2AuthenticationException.class)
.isThrownBy(() -> this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent))
.withMessageContaining("Invalid issuer")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo("invalid_nonce");
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenSubjectDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Jwt jwt = createJwt().subject("invalid").build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.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))
.withMessageContaining("Invalid subject")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenIssuedAtIsBeforeThenThrowsOAuth2AuthenticationException() {
Instant issuedAt = this.oidcUser.getIssuedAt().minus(2, ChronoUnit.MINUTES);
Jwt jwt = createJwt().issuedAt(issuedAt).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.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))
.withMessageContaining("Invalid issued at time")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenAudienceDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Jwt jwt = createJwt().audience(List.of("audience1", "audience3")).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.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))
.withMessageContaining("Invalid audience")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Instant authTime = this.oidcUser.getAuthenticatedAt().plus(5, ChronoUnit.SECONDS);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.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))
.withMessageContaining("Invalid authenticated at time")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenAuthenticatedAtMatchesThenOidcUserRefreshedEventPublished() {
Instant authTime = this.authentication.getPrincipal().getAttribute(IdTokenClaimNames.AUTH_TIME);
Jwt jwt = createJwt().claim(IdTokenClaimNames.AUTH_TIME, authTime).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
this.accessTokenResponse, this.authorizedClient);
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
verifyNoMoreInteractions(this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenNonceDoesNotMatchThenThrowsOAuth2AuthenticationException() {
Jwt jwt = createJwt().claim(IdTokenClaimNames.NONCE, "invalid").build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.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))
.withMessageContaining("Invalid nonce")
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo(INVALID_NONCE_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
verifyNoInteractions(this.userService, this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenIdTokenNonceMatchesThenOidcUserRefreshedEventPublished() {
Jwt jwt = createJwt().claim(IdTokenClaimNames.NONCE, this.oidcUser.getNonce()).build();
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
given(this.userService.loadUser(any(OidcUserRequest.class))).willReturn(this.oidcUser);
OAuth2AuthorizedClientRefreshedEvent authorizedClientRefreshedEvent = new OAuth2AuthorizedClientRefreshedEvent(
this.accessTokenResponse, this.authorizedClient);
this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent);
verify(this.applicationEventPublisher).publishEvent(any(OidcUserRefreshedEvent.class));
verifyNoMoreInteractions(this.applicationEventPublisher);
}
@Test
public void onApplicationEventWhenInvalidIdTokenThenThrowsOAuth2AuthenticationException() {
OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
SecurityContextImpl securityContext = new SecurityContextImpl(this.authentication);
given(this.securityContextHolderStrategy.getContext()).willReturn(securityContext);
given(this.jwtDecoder.decode(anyString())).willThrow(new JwtException("Invalid token"));
@ -326,7 +506,7 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
.isThrownBy(() -> this.eventListener.onApplicationEvent(authorizedClientRefreshedEvent))
.extracting(OAuth2AuthenticationException::getError)
.extracting(OAuth2Error::getErrorCode)
.isEqualTo("invalid_id_token");
.isEqualTo(INVALID_ID_TOKEN_ERROR);
verify(this.securityContextHolderStrategy).getContext();
verify(this.jwtDecoder).decode(this.jwt.getTokenValue());
verifyNoMoreInteractions(this.securityContextHolderStrategy, this.jwtDecoder);
@ -335,8 +515,7 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
@Test
public void onApplicationEventWhenCustomAuthoritiesMapperSetThenUsed() {
OAuth2AuthenticationToken authentication = createAuthenticationToken(this.clientRegistration);
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
SecurityContextImpl securityContext = new SecurityContextImpl(this.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);
@ -377,33 +556,36 @@ public class OidcAuthorizedClientRefreshedEventListenerTests {
.build();
}
private static Jwt createJwt() {
private static Jwt.Builder createJwt() {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
return TestJwts.jwt()
.issuer("https://surf.school")
.subject(SUBJECT)
.tokenValue(ID_TOKEN_VALUE)
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.claim(OidcParameterNames.NONCE, "nonce")
.build();
.audience(List.of("audience1", "audience2"));
}
private static OidcUser createOidcUser() {
Instant issuedAt = Instant.now().minus(30, ChronoUnit.SECONDS);
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
Map<String, Object> claims = new HashMap<>();
claims.put(IdTokenClaimNames.ISS, "https://surf.school");
claims.put(IdTokenClaimNames.SUB, SUBJECT);
claims.put(IdTokenClaimNames.ISS, "issuer");
claims.put(IdTokenClaimNames.IAT, issuedAt);
claims.put(IdTokenClaimNames.EXP, expiresAt);
claims.put(IdTokenClaimNames.AUD, List.of("audience1", "audience2"));
claims.put(IdTokenClaimNames.AUTH_TIME, issuedAt);
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();
private static OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration,
OidcUser oidcUser) {
return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
clientRegistration.getRegistrationId());
}