Skip to content

Commit db67f36

Browse files
committed
Fix ID Token auth_time validation (reactive)
Issue gh-18839 gh-17246
1 parent a8281a9 commit db67f36

2 files changed

Lines changed: 74 additions & 5 deletions

File tree

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,12 @@ private void validateAuthenticatedAt(OidcUser existingOidcUser, OidcIdToken idTo
282282
if (idToken.getAuthenticatedAt() == null) {
283283
return;
284284
}
285-
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) {
285+
// The auth_time claim MUST represent the time of the original authentication OR
286+
// the most recent time when the end-user reauthenticated when "prompt=login" is
287+
// passed in the authentication request
288+
if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())
289+
&& (existingOidcUser.getIdToken().getAuthenticatedAt() == null
290+
|| !idToken.getAuthenticatedAt().isAfter(existingOidcUser.getIdToken().getAuthenticatedAt()))) {
286291
OAuth2Error oauth2Error = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, "Invalid authenticated at time",
287292
REFRESH_TOKEN_RESPONSE_ERROR_URI);
288293
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818

1919
import java.time.Duration;
2020
import java.time.Instant;
21+
import java.time.temporal.ChronoUnit;
2122
import java.util.Collections;
2223
import java.util.HashMap;
24+
import java.util.LinkedHashSet;
2325
import java.util.List;
2426
import java.util.Map;
2527
import java.util.Set;
@@ -37,6 +39,7 @@
3739
import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
3840
import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
3941
import org.springframework.security.oauth2.core.OAuth2AccessToken;
42+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
4043
import org.springframework.security.oauth2.core.oidc.OidcScopes;
4144
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
4245
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
@@ -99,7 +102,8 @@ void setClockSkewWhenNullThenException() {
99102
@Test
100103
void onAuthorizationSuccessWhenIdTokenValidThenSecurityContextRefreshed() {
101104
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
102-
DefaultOidcUser principal = TestOidcUsers.create();
105+
Instant authTime = Instant.now();
106+
DefaultOidcUser principal = createOidcUser(authTime);
103107
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
104108
principal.getAuthorities(), clientRegistration.getRegistrationId());
105109
OAuth2AccessToken accessToken = createAccessToken();
@@ -112,6 +116,7 @@ void onAuthorizationSuccessWhenIdTokenValidThenSecurityContextRefreshed() {
112116
claims.put("iss", principal.getIssuer());
113117
claims.put("sub", principal.getSubject());
114118
claims.put("aud", principal.getAudience());
119+
claims.put("auth_time", authTime);
115120
claims.put("nonce", principal.getNonce());
116121
Jwt jwt = mock(Jwt.class);
117122
given(jwt.getTokenValue()).willReturn("id-token-1234");
@@ -316,9 +321,10 @@ void onAuthorizationSuccessWhenIdTokenAudNotContainThenException() {
316321
}
317322

318323
@Test
319-
void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() {
324+
void onAuthorizationSuccessWhenIdTokenAuthTimeBeforeCurrentAuthTimeThenException() {
320325
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
321-
DefaultOidcUser principal = TestOidcUsers.create();
326+
Instant authTime = Instant.now();
327+
DefaultOidcUser principal = createOidcUser(authTime);
322328
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
323329
principal.getAuthorities(), clientRegistration.getRegistrationId());
324330
OAuth2AccessToken accessToken = createAccessToken();
@@ -331,7 +337,7 @@ void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() {
331337
claims.put("iss", principal.getIssuer());
332338
claims.put("sub", principal.getSubject());
333339
claims.put("aud", principal.getAudience());
334-
claims.put("auth_time", principal.getIssuedAt());
340+
claims.put("auth_time", authTime.minus(5, ChronoUnit.MINUTES));
335341
claims.put("nonce", principal.getNonce());
336342
Jwt jwt = mock(Jwt.class);
337343
given(jwt.getTokenValue()).willReturn("id-token-1234");
@@ -352,6 +358,47 @@ void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() {
352358
.verifyErrorMessage("[invalid_id_token] Invalid authenticated at time");
353359
}
354360

361+
@Test
362+
void onAuthorizationSuccessWhenIdTokenAuthTimeAfterCurrentAuthTimeThenSecurityContextRefreshed() {
363+
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
364+
Instant authTime = Instant.now();
365+
DefaultOidcUser principal = createOidcUser(authTime);
366+
OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal,
367+
principal.getAuthorities(), clientRegistration.getRegistrationId());
368+
OAuth2AccessToken accessToken = createAccessToken();
369+
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(),
370+
accessToken, null);
371+
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build());
372+
Map<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
373+
OidcParameterNames.ID_TOKEN, "id-token-1234");
374+
Map<String, Object> claims = new HashMap<>();
375+
claims.put("iss", principal.getIssuer());
376+
claims.put("sub", principal.getSubject());
377+
claims.put("aud", principal.getAudience());
378+
claims.put("auth_time", authTime.plus(5, ChronoUnit.MINUTES));
379+
claims.put("nonce", principal.getNonce());
380+
Jwt jwt = mock(Jwt.class);
381+
given(jwt.getTokenValue()).willReturn("id-token-1234");
382+
given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt());
383+
given(jwt.getClaims()).willReturn(claims);
384+
ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class);
385+
given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt));
386+
ReactiveJwtDecoderFactory<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
387+
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
388+
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService = mock(ReactiveOAuth2UserService.class);
389+
given(userService.loadUser(any())).willReturn(Mono.just(principal));
390+
WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository();
391+
RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler();
392+
handler.setJwtDecoderFactory(reactiveJwtDecoderFactory);
393+
handler.setUserService(userService);
394+
handler.setServerSecurityContextRepository(serverSecurityContextRepository);
395+
StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes))
396+
.verifyComplete();
397+
StepVerifier.create(serverSecurityContextRepository.load(exchange).map(SecurityContext::getAuthentication))
398+
.expectNext(authenticationToken)
399+
.verifyComplete();
400+
}
401+
355402
@Test
356403
void onAuthorizationSuccessWhenIdTokenNonceNotSameThenException() {
357404
ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build();
@@ -395,4 +442,21 @@ private static OAuth2AccessToken createAccessToken() {
395442
Set.of(OidcScopes.OPENID));
396443
}
397444

445+
private static DefaultOidcUser createOidcUser(Instant authTime) {
446+
Instant issuedAt = Instant.now();
447+
Instant expiresAt = issuedAt.plusSeconds(3600);
448+
// @formatter:off
449+
OidcIdToken idToken = OidcIdToken.withTokenValue("id-token")
450+
.issuedAt(issuedAt)
451+
.expiresAt(expiresAt)
452+
.subject("subject")
453+
.issuer("http://localhost/issuer")
454+
.audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client-id"))))
455+
.authorizedParty("client")
456+
.authTime(authTime)
457+
.build();
458+
// @formatter:on
459+
return new DefaultOidcUser(null, idToken);
460+
}
461+
398462
}

0 commit comments

Comments
 (0)