Fix auth_time claim should represent authentication time

Closes gh-18282
This commit is contained in:
Joe Grandja 2026-04-07 15:38:00 -04:00
parent 2361dc131e
commit 41524880c6
4 changed files with 40 additions and 11 deletions

View File

@ -60,6 +60,7 @@ import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
@ -210,7 +211,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.with(user("user").roles("A", "B")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@ -270,7 +272,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user").roles("A", "B")))
.with(user("user").roles("A", "B")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@ -335,7 +338,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(issuer.concat(DEFAULT_AUTHORIZATION_ENDPOINT_URI)).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@ -388,7 +392,8 @@ public class OidcTests {
registeredClient1);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user1")))
.with(user("user1")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@ -424,7 +429,8 @@ public class OidcTests {
authorizationRequestParameters = getAuthorizationRequestParameters(registeredClient2);
mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user2")))
.with(user("user2")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
@ -497,7 +503,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
@ -537,7 +544,8 @@ public class OidcTests {
registeredClient);
MvcResult mvcResult = this.mvc
.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI).queryParams(authorizationRequestParameters)
.with(user("user")))
.with(user("user")
.authorities(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY))))
.andExpect(status().is3xxRedirection())
.andReturn();
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();

View File

@ -24,6 +24,9 @@ import java.util.Date;
import java.util.UUID;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@ -141,7 +144,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
SessionInformation sessionInformation = context.get(SessionInformation.class);
if (sessionInformation != null) {
claimsBuilder.claim("sid", sessionInformation.getSessionId());
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, sessionInformation.getLastRequest());
claimsBuilder.claim(IdTokenClaimNames.AUTH_TIME, getAuthenticationTime(context.getPrincipal()));
}
}
else if (AuthorizationGrantType.REFRESH_TOKEN.equals(context.getAuthorizationGrantType())) {
@ -222,4 +225,17 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
this.clock = clock;
}
static Date getAuthenticationTime(Authentication authentication) {
Instant authenticationTime = null;
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (grantedAuthority instanceof FactorGrantedAuthority factorGrantedAuthority) {
if (authenticationTime == null || factorGrantedAuthority.getIssuedAt().isAfter(authenticationTime)) {
authenticationTime = factorGrantedAuthority.getIssuedAt();
}
}
}
Assert.notNull(authenticationTime, "authenticationTime cannot be null");
return Date.from(authenticationTime);
}
}

View File

@ -24,6 +24,9 @@ import java.util.HashMap;
import java.util.Map;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
@ -85,6 +88,9 @@ public final class TestOAuth2Authorizations {
.additionalParameters(authorizationRequestAdditionalParameters)
.state("state")
.build();
Authentication principal = new TestingAuthenticationToken("principal", null,
new SimpleGrantedAuthority("ROLE_A"), new SimpleGrantedAuthority("ROLE_B"),
FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
OAuth2Authorization.Builder builder = OAuth2Authorization.withRegisteredClient(registeredClient)
.id("id")
.principalName("principal")
@ -93,8 +99,7 @@ public final class TestOAuth2Authorizations {
.token(authorizationCode)
.attribute(OAuth2ParameterNames.STATE, "consent-state")
.attribute(OAuth2AuthorizationRequest.class.getName(), authorizationRequest)
.attribute(Principal.class.getName(),
new TestingAuthenticationToken("principal", null, "ROLE_A", "ROLE_B"));
.attribute(Principal.class.getName(), principal);
if (accessToken != null) {
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken("refresh-token", Instant.now(),
Instant.now().plus(1, ChronoUnit.HOURS));

View File

@ -363,7 +363,7 @@ public class JwtGeneratorTests {
SessionInformation sessionInformation = tokenContext.get(SessionInformation.class);
assertThat(jwtClaimsSet.<String>getClaim("sid")).isEqualTo(sessionInformation.getSessionId());
assertThat(jwtClaimsSet.<Date>getClaim(IdTokenClaimNames.AUTH_TIME))
.isEqualTo(sessionInformation.getLastRequest());
.isEqualTo(JwtGenerator.getAuthenticationTime(tokenContext.getPrincipal()));
}
else if (tokenContext.getAuthorizationGrantType().equals(AuthorizationGrantType.REFRESH_TOKEN)) {
OidcIdToken currentIdToken = tokenContext.getAuthorization().getToken(OidcIdToken.class).getToken();