Skip to content

Commit 860f130

Browse files
committed
Add additional validation when refreshing ID tokens
Issue gh-16589
1 parent 5f98ce5 commit 860f130

File tree

3 files changed

+340
-47
lines changed

3 files changed

+340
-47
lines changed

Diff for: config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OidcUserRefreshedEventListenerConfigurationTests.java

+23-15
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ public void authorizeWhenAccessTokenResponseMissingOpenidScopeThenOidcUserNotRef
171171
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
172172
.willReturn(accessTokenResponse);
173173

174-
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
174+
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
175+
createOidcUser());
175176
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
176177
.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
177178
.principal(authentication)
@@ -194,7 +195,8 @@ public void authorizeWhenAccessTokenResponseMissingIdTokenThenOidcUserNotRefresh
194195
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
195196
.willReturn(accessTokenResponse);
196197

197-
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
198+
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
199+
createOidcUser());
198200
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest
199201
.withClientRegistrationId(GOOGLE_CLIENT_REGISTRATION.getRegistrationId())
200202
.principal(authentication)
@@ -297,7 +299,8 @@ public void authorizeWhenAuthenticationClientRegistrationIdDoesNotMatchThenOidcU
297299
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
298300
.willReturn(accessTokenResponse);
299301

300-
OAuth2AuthenticationToken authentication = createAuthenticationToken(GITHUB_CLIENT_REGISTRATION);
302+
OAuth2AuthenticationToken authentication = createAuthenticationToken(GITHUB_CLIENT_REGISTRATION,
303+
createOidcUser());
301304
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
302305
SecurityContextHolder.setContext(securityContext);
303306

@@ -316,17 +319,17 @@ public void authorizeWhenAccessTokenResponseIncludesIdTokenThenOidcUserRefreshed
316319

317320
OAuth2AuthorizedClient authorizedClient = createAuthorizedClient();
318321
OAuth2AccessTokenResponse accessTokenResponse = createAccessTokenResponse(OidcScopes.OPENID);
319-
Jwt jwt = createJwt();
320-
OidcUser oidcUser = createOidcUser();
322+
Jwt jwt = createJwt().build();
321323
given(this.authorizedClientRepository.loadAuthorizedClient(anyString(), any(Authentication.class),
322324
any(HttpServletRequest.class)))
323325
.willReturn(authorizedClient);
324326
given(this.refreshTokenAccessTokenResponseClient.getTokenResponse(any(OAuth2RefreshTokenGrantRequest.class)))
325327
.willReturn(accessTokenResponse);
326328
given(this.jwtDecoder.decode(anyString())).willReturn(jwt);
327-
given(this.oidcUserService.loadUser(any(OidcUserRequest.class))).willReturn(oidcUser);
329+
given(this.oidcUserService.loadUser(any(OidcUserRequest.class))).willReturn(createOidcUser());
328330

329-
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION);
331+
OAuth2AuthenticationToken authentication = createAuthenticationToken(GOOGLE_CLIENT_REGISTRATION,
332+
createOidcUser());
330333
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
331334
SecurityContextHolder.setContext(securityContext);
332335

@@ -405,31 +408,36 @@ private OAuth2AccessTokenResponse createAccessTokenResponse(String... scope) {
405408
.build();
406409
}
407410

408-
private Jwt createJwt() {
411+
private static Jwt.Builder createJwt() {
409412
Instant issuedAt = Instant.now();
410413
Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
411414
return TestJwts.jwt()
415+
.issuer("https://surf.school")
412416
.subject(SUBJECT)
413417
.tokenValue(ID_TOKEN_VALUE)
414418
.issuedAt(issuedAt)
415419
.expiresAt(expiresAt)
416-
.build();
420+
.audience(List.of("audience1", "audience2"));
417421
}
418422

419-
private OidcUser createOidcUser() {
423+
private static OidcUser createOidcUser() {
424+
Instant issuedAt = Instant.now().minus(30, ChronoUnit.SECONDS);
425+
Instant expiresAt = issuedAt.plus(5, ChronoUnit.MINUTES);
420426
Map<String, Object> claims = new HashMap<>();
427+
claims.put(IdTokenClaimNames.ISS, "https://surf.school");
421428
claims.put(IdTokenClaimNames.SUB, SUBJECT);
422-
claims.put(IdTokenClaimNames.ISS, "issuer");
429+
claims.put(IdTokenClaimNames.IAT, issuedAt);
430+
claims.put(IdTokenClaimNames.EXP, expiresAt);
423431
claims.put(IdTokenClaimNames.AUD, List.of("audience1", "audience2"));
424-
Instant issuedAt = Instant.now();
425-
Instant expiresAt = issuedAt.plus(1, ChronoUnit.MINUTES);
432+
claims.put(IdTokenClaimNames.AUTH_TIME, issuedAt);
433+
claims.put(IdTokenClaimNames.NONCE, "nonce");
426434
OidcIdToken idToken = new OidcIdToken(ID_TOKEN_VALUE, issuedAt, expiresAt, claims);
427435

428436
return new DefaultOidcUser(AuthorityUtils.createAuthorityList("OIDC_USER"), idToken);
429437
}
430438

431-
private OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration) {
432-
OidcUser oidcUser = createOidcUser();
439+
private OAuth2AuthenticationToken createAuthenticationToken(ClientRegistration clientRegistration,
440+
OidcUser oidcUser) {
433441
return new OAuth2AuthenticationToken(oidcUser, oidcUser.getAuthorities(),
434442
clientRegistration.getRegistrationId());
435443
}

Diff for: oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizedClientRefreshedEventListener.java

+106-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
package org.springframework.security.oauth2.client.oidc.authentication;
1818

19+
import java.time.Duration;
1920
import java.util.Collection;
21+
import java.util.HashSet;
22+
import java.util.List;
2023
import java.util.Map;
24+
import java.util.Set;
2125

2226
import org.springframework.context.ApplicationEventPublisher;
2327
import org.springframework.context.ApplicationEventPublisherAware;
@@ -67,6 +71,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
6771

6872
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
6973

74+
private static final String REFRESH_TOKEN_RESPONSE_ERROR_URI = "https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse";
75+
7076
private OAuth2UserService<OidcUserRequest, OidcUser> userService = new OidcUserService();
7177

7278
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
@@ -78,6 +84,8 @@ public final class OidcAuthorizedClientRefreshedEventListener
7884

7985
private ApplicationEventPublisher applicationEventPublisher;
8086

87+
private Duration clockSkew = Duration.ofSeconds(60);
88+
8189
@Override
8290
public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
8391
if (this.applicationEventPublisher == null) {
@@ -119,7 +127,7 @@ public void onApplicationEvent(OAuth2AuthorizedClientRefreshedEvent event) {
119127

120128
// Refresh the OidcUser and send a user refreshed event
121129
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
122-
validateNonce(existingOidcUser, idToken);
130+
validateIdToken(existingOidcUser, idToken);
123131
OidcUserRequest userRequest = new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(),
124132
idToken, additionalParameters);
125133
OidcUser oidcUser = this.userService.loadUser(userRequest);
@@ -187,6 +195,17 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
187195
this.applicationEventPublisher = applicationEventPublisher;
188196
}
189197

198+
/**
199+
* Sets the maximum acceptable clock skew, which is used when checking the
200+
* {@link OidcIdToken#getIssuedAt() issuedAt} time. The default is 60 seconds.
201+
* @param clockSkew the maximum acceptable clock skew
202+
*/
203+
public void setClockSkew(Duration clockSkew) {
204+
Assert.notNull(clockSkew, "clockSkew cannot be null");
205+
Assert.isTrue(clockSkew.getSeconds() >= 0, "clockSkew must be >= 0");
206+
this.clockSkew = clockSkew;
207+
}
208+
190209
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
191210
OAuth2AccessTokenResponse accessTokenResponse) {
192211
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
@@ -205,13 +224,97 @@ private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwt
205224
}
206225
}
207226

227+
private void validateIdToken(OidcUser existingOidcUser, OidcIdToken idToken) {
228+
// OpenID Connect Core 1.0 - Section 12.2 Successful Refresh Response
229+
// If an ID Token is returned as a result of a token refresh request, the
230+
// following requirements apply:
231+
// its iss Claim Value MUST be the same as in the ID Token issued when the
232+
// original authentication occurred,
233+
validateIssuer(existingOidcUser, idToken);
234+
// its sub Claim Value MUST be the same as in the ID Token issued when the
235+
// original authentication occurred,
236+
validateSubject(existingOidcUser, idToken);
237+
// its iat Claim MUST represent the time that the new ID Token is issued,
238+
validateIssuedAt(existingOidcUser, idToken);
239+
// its aud Claim Value MUST be the same as in the ID Token issued when the
240+
// original authentication occurred,
241+
validateAudience(existingOidcUser, idToken);
242+
// if the ID Token contains an auth_time Claim, its value MUST represent the time
243+
// of the original authentication - not the time that the new ID token is issued,
244+
validateAuthenticatedAt(existingOidcUser, idToken);
245+
// it SHOULD NOT have a nonce Claim, even when the ID Token issued at the time of
246+
// the original authentication contained nonce; however, if it is present, its
247+
// value MUST be the same as in the ID Token issued at the time of the original
248+
// authentication,
249+
validateNonce(existingOidcUser, idToken);
250+
}
251+
252+
private void validateIssuer(OidcUser existingOidcUser, OidcIdToken idToken) {
253+
if (!idToken.getIssuer().toString().equals(existingOidcUser.getIdToken().getIssuer().toString())) {
254+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issuer",
255+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
256+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
257+
}
258+
}
259+
260+
private void validateSubject(OidcUser existingOidcUser, OidcIdToken idToken) {
261+
if (!idToken.getSubject().equals(existingOidcUser.getIdToken().getSubject())) {
262+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid subject",
263+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
264+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
265+
}
266+
}
267+
268+
private void validateIssuedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
269+
if (!idToken.getIssuedAt().isAfter(existingOidcUser.getIdToken().getIssuedAt().minus(this.clockSkew))) {
270+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid issued at time",
271+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
272+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
273+
}
274+
}
275+
276+
private void validateAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
277+
if (!isValidAudience(existingOidcUser, idToken)) {
278+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid audience",
279+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
280+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
281+
}
282+
}
283+
284+
private boolean isValidAudience(OidcUser existingOidcUser, OidcIdToken idToken) {
285+
List<String> idTokenAudiences = idToken.getAudience();
286+
Set<String> oidcUserAudiences = new HashSet<>(existingOidcUser.getIdToken().getAudience());
287+
if (idTokenAudiences.size() != oidcUserAudiences.size()) {
288+
return false;
289+
}
290+
for (String audience : idTokenAudiences) {
291+
if (!oidcUserAudiences.contains(audience)) {
292+
return false;
293+
}
294+
}
295+
return true;
296+
}
297+
298+
private void validateAuthenticatedAt(OidcUser existingOidcUser, OidcIdToken idToken) {
299+
if (idToken.getAuthenticatedAt() == null) {
300+
return;
301+
}
302+
303+
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
304+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
305+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
306+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
307+
}
308+
}
309+
208310
private void validateNonce(OidcUser existingOidcUser, OidcIdToken idToken) {
209311
if (!StringUtils.hasText(idToken.getNonce())) {
210312
return;
211313
}
212314

213-
if (!idToken.getNonce().equals(existingOidcUser.getNonce())) {
214-
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
315+
if (!idToken.getNonce().equals(existingOidcUser.getIdToken().getNonce())) {
316+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE, "Invalid nonce",
317+
REFRESH_TOKEN_RESPONSE_ERROR_URI);
215318
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
216319
}
217320
}

0 commit comments

Comments
 (0)