mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-31 01:02:14 +00:00
Add additional validation when refreshing ID tokens
Issue gh-16589
This commit is contained in:
parent
5f98ce5ecc
commit
860f130bc4
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user