Allow setting Clock in OAuth2TokenGenerator implementations
CodeQL Advanced / codeql-analysis-call (push) Waiting to run Details
CI / Build (17, ubuntu-latest) (push) Waiting to run Details
CI / Build (17, windows-latest) (push) Waiting to run Details
CI / Deploy Artifacts (push) Blocked by required conditions Details
CI / Deploy Docs (push) Blocked by required conditions Details
CI / Deploy Schema (push) Blocked by required conditions Details
CI / Perform Release (push) Blocked by required conditions Details
CI / Send Notification (push) Blocked by required conditions Details
Deploy Docs / build (push) Waiting to run Details

Closes gh-18017
This commit is contained in:
Joe Grandja 2025-10-07 11:51:42 -04:00
parent 1d7f4c3b11
commit 469ed09645
6 changed files with 89 additions and 7 deletions

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Collections; import java.util.Collections;
@ -65,6 +66,8 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer; private OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer;
private Clock clock = Clock.systemUTC();
/** /**
* Constructs a {@code JwtGenerator} using the provided parameters. * Constructs a {@code JwtGenerator} using the provided parameters.
* @param jwtEncoder the jwt encoder * @param jwtEncoder the jwt encoder
@ -95,7 +98,7 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
} }
RegisteredClient registeredClient = context.getRegisteredClient(); RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now(); Instant issuedAt = this.clock.instant();
Instant expiresAt; Instant expiresAt;
JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256; JwsAlgorithm jwsAlgorithm = SignatureAlgorithm.RS256;
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
@ -208,4 +211,15 @@ public final class JwtGenerator implements OAuth2TokenGenerator<Jwt> {
this.jwtCustomizer = jwtCustomizer; this.jwtCustomizer = jwtCustomizer;
} }
/**
* Sets the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}.
* @param clock the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
} }

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.util.Base64; import java.util.Base64;
import java.util.Collections; import java.util.Collections;
@ -56,6 +57,8 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer; private OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer;
private Clock clock = Clock.systemUTC();
@Nullable @Nullable
@Override @Override
public OAuth2AccessToken generate(OAuth2TokenContext context) { public OAuth2AccessToken generate(OAuth2TokenContext context) {
@ -72,7 +75,7 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
} }
RegisteredClient registeredClient = context.getRegisteredClient(); RegisteredClient registeredClient = context.getRegisteredClient();
Instant issuedAt = Instant.now(); Instant issuedAt = this.clock.instant();
Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive()); Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
// @formatter:off // @formatter:off
@ -140,6 +143,17 @@ public final class OAuth2AccessTokenGenerator implements OAuth2TokenGenerator<OA
this.accessTokenCustomizer = accessTokenCustomizer; this.accessTokenCustomizer = accessTokenCustomizer;
} }
/**
* Sets the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}.
* @param clock the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor { private static final class OAuth2AccessTokenClaims extends OAuth2AccessToken implements ClaimAccessor {
private final Map<String, Object> claims; private final Map<String, Object> claims;

View File

@ -16,6 +16,7 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.util.Base64; import java.util.Base64;
@ -27,6 +28,7 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.util.Assert;
/** /**
* An {@link OAuth2TokenGenerator} that generates an {@link OAuth2RefreshToken}. * An {@link OAuth2TokenGenerator} that generates an {@link OAuth2RefreshToken}.
@ -41,6 +43,8 @@ public final class OAuth2RefreshTokenGenerator implements OAuth2TokenGenerator<O
private final StringKeyGenerator refreshTokenGenerator = new Base64StringKeyGenerator( private final StringKeyGenerator refreshTokenGenerator = new Base64StringKeyGenerator(
Base64.getUrlEncoder().withoutPadding(), 96); Base64.getUrlEncoder().withoutPadding(), 96);
private Clock clock = Clock.systemUTC();
@Nullable @Nullable
@Override @Override
public OAuth2RefreshToken generate(OAuth2TokenContext context) { public OAuth2RefreshToken generate(OAuth2TokenContext context) {
@ -52,11 +56,22 @@ public final class OAuth2RefreshTokenGenerator implements OAuth2TokenGenerator<O
return null; return null;
} }
Instant issuedAt = Instant.now(); Instant issuedAt = this.clock.instant();
Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive()); Instant expiresAt = issuedAt.plus(context.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
return new OAuth2RefreshToken(this.refreshTokenGenerator.generateKey(), issuedAt, expiresAt); return new OAuth2RefreshToken(this.refreshTokenGenerator.generateKey(), issuedAt, expiresAt);
} }
/**
* Sets the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}.
* @param clock the {@link Clock} used when obtaining the current instant via
* {@link Clock#instant()}
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
private static boolean isPublicClientForAuthorizationCodeGrant(OAuth2TokenContext context) { private static boolean isPublicClientForAuthorizationCodeGrant(OAuth2TokenContext context) {
// @formatter:off // @formatter:off
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) && if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getAuthorizationGrantType()) &&

View File

@ -17,6 +17,8 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.security.Principal; import java.security.Principal;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
@ -104,6 +106,12 @@ public class JwtGeneratorTests {
.withMessage("jwtCustomizer cannot be null"); .withMessage("jwtCustomizer cannot be null");
} }
@Test
public void setClockWhenNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> this.jwtGenerator.setClock(null))
.withMessage("clock cannot be null");
}
@Test @Test
public void generateWhenUnsupportedTokenTypeThenReturnNull() { public void generateWhenUnsupportedTokenTypeThenReturnNull() {
// @formatter:off // @formatter:off
@ -158,7 +166,10 @@ public class JwtGeneratorTests {
.build(); .build();
// @formatter:on // @formatter:on
assertGeneratedTokenType(tokenContext); Clock clock = Clock.offset(Clock.systemUTC(), Duration.ofMinutes(5));
this.jwtGenerator.setClock(clock);
assertGeneratedTokenType(tokenContext, clock);
} }
@Test @Test
@ -282,6 +293,10 @@ public class JwtGeneratorTests {
} }
private void assertGeneratedTokenType(OAuth2TokenContext tokenContext) { private void assertGeneratedTokenType(OAuth2TokenContext tokenContext) {
assertGeneratedTokenType(tokenContext, Clock.systemUTC());
}
private void assertGeneratedTokenType(OAuth2TokenContext tokenContext, Clock clock) {
this.jwtGenerator.generate(tokenContext); this.jwtGenerator.generate(tokenContext);
ArgumentCaptor<JwtEncodingContext> jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class); ArgumentCaptor<JwtEncodingContext> jwtEncodingContextCaptor = ArgumentCaptor.forClass(JwtEncodingContext.class);
@ -318,7 +333,7 @@ public class JwtGeneratorTests {
assertThat(jwtClaimsSet.getSubject()).isEqualTo(tokenContext.getAuthorization().getPrincipalName()); assertThat(jwtClaimsSet.getSubject()).isEqualTo(tokenContext.getAuthorization().getPrincipalName());
assertThat(jwtClaimsSet.getAudience()).containsExactly(tokenContext.getRegisteredClient().getClientId()); assertThat(jwtClaimsSet.getAudience()).containsExactly(tokenContext.getRegisteredClient().getClientId());
Instant issuedAt = Instant.now(); Instant issuedAt = clock.instant();
Instant expiresAt; Instant expiresAt;
if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) { if (tokenContext.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive()); expiresAt = issuedAt.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive());

View File

@ -17,6 +17,8 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.security.Principal; import java.security.Principal;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
@ -81,6 +83,13 @@ public class OAuth2AccessTokenGeneratorTests {
.withMessage("accessTokenCustomizer cannot be null"); .withMessage("accessTokenCustomizer cannot be null");
} }
@Test
public void setClockWhenNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.accessTokenGenerator.setClock(null))
.withMessage("clock cannot be null");
}
@Test @Test
public void generateWhenUnsupportedTokenTypeThenReturnNull() { public void generateWhenUnsupportedTokenTypeThenReturnNull() {
// @formatter:off // @formatter:off
@ -150,10 +159,13 @@ public class OAuth2AccessTokenGeneratorTests {
.build(); .build();
// @formatter:on // @formatter:on
Clock clock = Clock.offset(Clock.systemUTC(), Duration.ofMinutes(5));
this.accessTokenGenerator.setClock(clock);
OAuth2AccessToken accessToken = this.accessTokenGenerator.generate(tokenContext); OAuth2AccessToken accessToken = this.accessTokenGenerator.generate(tokenContext);
assertThat(accessToken).isNotNull(); assertThat(accessToken).isNotNull();
Instant issuedAt = Instant.now(); Instant issuedAt = clock.instant();
Instant expiresAt = issuedAt Instant expiresAt = issuedAt
.plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive()); .plus(tokenContext.getRegisteredClient().getTokenSettings().getAccessTokenTimeToLive());
assertThat(accessToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); assertThat(accessToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1));

View File

@ -16,6 +16,8 @@
package org.springframework.security.oauth2.server.authorization.token; package org.springframework.security.oauth2.server.authorization.token;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -26,6 +28,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients; import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/** /**
* Tests for {@link OAuth2RefreshTokenGenerator}. * Tests for {@link OAuth2RefreshTokenGenerator}.
@ -36,6 +39,12 @@ public class OAuth2RefreshTokenGeneratorTests {
private final OAuth2RefreshTokenGenerator tokenGenerator = new OAuth2RefreshTokenGenerator(); private final OAuth2RefreshTokenGenerator tokenGenerator = new OAuth2RefreshTokenGenerator();
@Test
public void setClockWhenNullThenThrowIllegalArgumentException() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> this.tokenGenerator.setClock(null))
.withMessage("clock cannot be null");
}
@Test @Test
public void generateWhenUnsupportedTokenTypeThenReturnNull() { public void generateWhenUnsupportedTokenTypeThenReturnNull() {
// @formatter:off // @formatter:off
@ -58,10 +67,13 @@ public class OAuth2RefreshTokenGeneratorTests {
.build(); .build();
// @formatter:on // @formatter:on
Clock clock = Clock.offset(Clock.systemUTC(), Duration.ofMinutes(5));
this.tokenGenerator.setClock(clock);
OAuth2RefreshToken refreshToken = this.tokenGenerator.generate(tokenContext); OAuth2RefreshToken refreshToken = this.tokenGenerator.generate(tokenContext);
assertThat(refreshToken).isNotNull(); assertThat(refreshToken).isNotNull();
Instant issuedAt = Instant.now(); Instant issuedAt = clock.instant();
Instant expiresAt = issuedAt Instant expiresAt = issuedAt
.plus(tokenContext.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive()); .plus(tokenContext.getRegisteredClient().getTokenSettings().getRefreshTokenTimeToLive());
assertThat(refreshToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1)); assertThat(refreshToken.getIssuedAt()).isBetween(issuedAt.minusSeconds(1), issuedAt.plusSeconds(1));