Browse Source

Fix ID Token auth_time validation (reactive)

Issue gh-18839 gh-17246
pull/18980/head
Joe Grandja 6 days ago
parent
commit
db67f36492
  1. 7
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler.java
  2. 72
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandlerTests.java

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

@ -282,7 +282,12 @@ public final class RefreshOidcUserReactiveOAuth2AuthorizationSuccessHandler @@ -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());

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

@ -18,8 +18,10 @@ package org.springframework.security.oauth2.client; @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<String, Object> attributes = Map.of(ServerWebExchange.class.getName(), exchange,
OidcParameterNames.ID_TOKEN, "id-token-1234");
Map<String, Object> 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<ClientRegistration> reactiveJwtDecoderFactory = mock(ReactiveJwtDecoderFactory.class);
given(reactiveJwtDecoderFactory.createDecoder(any())).willReturn(jwtDecoder);
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> 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 { @@ -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);
}
}

Loading…
Cancel
Save