diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java index c62b6c41fb..38aff0d049 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java @@ -282,7 +282,12 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler if (idToken.getAuthenticatedAt() == null) { return; } - if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt())) { + // The auth_time claim MUST represent the time of the original authentication OR + // the most recent time when the end-user reauthenticated when "prompt=login" is + // passed in the authentication request + if (!idToken.getAuthenticatedAt().equals(existingOidcUser.getIdToken().getAuthenticatedAt()) + && (existingOidcUser.getIdToken().getAuthenticatedAt() == null + || !idToken.getAuthenticatedAt().isAfter(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()); diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java index dd276be20a..6e4d337a62 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java @@ -18,8 +18,10 @@ package org.springframework.security.oauth2.client; import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -37,6 +39,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AccessToken; +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; @@ -99,7 +102,8 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { @Test void onAuthorizationSuccessWhenIdTokenValidThenSecurityContextRefreshed() { ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); - DefaultOidcUser principal = TestOidcUsers.create(); + Instant authTime = Instant.now(); + DefaultOidcUser principal = createOidcUser(authTime); OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal, principal.getAuthorities(), clientRegistration.getRegistrationId()); OAuth2AccessToken accessToken = createAccessToken(); @@ -112,6 +116,7 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { claims.put("iss", principal.getIssuer()); claims.put("sub", principal.getSubject()); claims.put("aud", principal.getAudience()); + claims.put("auth_time", authTime); claims.put("nonce", principal.getNonce()); Jwt jwt = mock(Jwt.class); given(jwt.getTokenValue()).willReturn("id-token-1234"); @@ -316,9 +321,10 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { } @Test - void onAuthorizationSuccessWhenIdTokenAuthTimeNotSameThenException() { + void onAuthorizationSuccessWhenIdTokenAuthTimeBeforeCurrentAuthTimeThenException() { ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); - DefaultOidcUser principal = TestOidcUsers.create(); + Instant authTime = Instant.now(); + DefaultOidcUser principal = createOidcUser(authTime); OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal, principal.getAuthorities(), clientRegistration.getRegistrationId()); OAuth2AccessToken accessToken = createAccessToken(); @@ -331,7 +337,7 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { claims.put("iss", principal.getIssuer()); claims.put("sub", principal.getSubject()); claims.put("aud", principal.getAudience()); - claims.put("auth_time", principal.getIssuedAt()); + claims.put("auth_time", authTime.minus(5, ChronoUnit.MINUTES)); claims.put("nonce", principal.getNonce()); Jwt jwt = mock(Jwt.class); given(jwt.getTokenValue()).willReturn("id-token-1234"); @@ -352,6 +358,47 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { .verifyErrorMessage("[invalid_id_token] Invalid authenticated at time"); } + @Test + void onAuthorizationSuccessWhenIdTokenAuthTimeAfterCurrentAuthTimeThenSecurityContextRefreshed() { + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); + Instant authTime = Instant.now(); + DefaultOidcUser principal = createOidcUser(authTime); + OAuth2AuthenticationToken authenticationToken = new OAuth2AuthenticationToken(principal, + principal.getAuthorities(), clientRegistration.getRegistrationId()); + OAuth2AccessToken accessToken = createAccessToken(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, principal.getName(), + accessToken, null); + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").build()); + Map attributes = Map.of(ServerWebExchange.class.getName(), exchange, + OidcParameterNames.ID_TOKEN, "id-token-1234"); + Map claims = new HashMap<>(); + claims.put("iss", principal.getIssuer()); + claims.put("sub", principal.getSubject()); + claims.put("aud", principal.getAudience()); + claims.put("auth_time", authTime.plus(5, ChronoUnit.MINUTES)); + claims.put("nonce", principal.getNonce()); + Jwt jwt = mock(Jwt.class); + given(jwt.getTokenValue()).willReturn("id-token-1234"); + given(jwt.getIssuedAt()).willReturn(principal.getIssuedAt()); + given(jwt.getClaims()).willReturn(claims); + ReactiveJwtDecoder jwtDecoder = mock(ReactiveJwtDecoder.class); + given(jwtDecoder.decode(any())).willReturn(Mono.just(jwt)); + ReactiveJwtDecoderFactory reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class); + given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder); + ReactiveOAuth2UserService userService = mock(ReactiveOAuth2UserService.class); + given(userService.loadUser(any())).willReturn(Mono.just(principal)); + WebSessionServerSecurityContextRepository serverSecurityContextRepository = new WebSessionServerSecurityContextRepository(); + RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler handler = new RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler(); + handler.setJwtDecoderFactory(reactiveJwtDecoderFactory); + handler.setUserService(userService); + handler.setServerSecurityContextRepository(serverSecurityContextRepository); + StepVerifier.create(handler.onAuthorizationSuccess(authorizedClient, authenticationToken, attributes)) + .verifyComplete(); + StepVerifier.create(serverSecurityContextRepository.load(exchange).map(SecurityContext::getAuthentication)) + .expectNext(authenticationToken) + .verifyComplete(); + } + @Test void onAuthorizationSuccessWhenIdTokenNonceNotSameThenException() { ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration().build(); @@ -395,4 +442,21 @@ class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests { Set.of(OidcScopes.OPENID)); } + private static DefaultOidcUser createOidcUser(Instant authTime) { + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plusSeconds(3600); + // @formatter:off + OidcIdToken idToken = OidcIdToken.withTokenValue("id-token") + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .subject("subject") + .issuer("http://localhost/issuer") + .audience(Collections.unmodifiableSet(new LinkedHashSet<>(Collections.singletonList("client-id")))) + .authorizedParty("client") + .authTime(authTime) + .build(); + // @formatter:on + return new DefaultOidcUser(null, idToken); + } + }