From 5821cff27f7e3c0b1bd5ad1e123f5b78d9c75bf2 Mon Sep 17 00:00:00 2001 From: Ferenc Kemeny Date: Thu, 8 May 2025 22:40:47 +0200 Subject: [PATCH 1/2] Polish JwtTimestampValidatorTests This commit corrects the test that checks for both nbf and exp missing. It also adds one for just exp and on for just nbf. Issue gh-17004 Signed-off-by: Ferenc Kemeny --- .../oauth2/jwt/JwtTimestampValidatorTests.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 72164cf21b7..272004a5ec4 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -129,6 +129,23 @@ public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() { @Test public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = TestJwts.jwt().claims((c) -> { + c.remove(JwtClaimNames.EXP); + c.remove(JwtClaimNames.NBF); + }).build(); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenExpiryIsSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.EXP)).build(); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); + } + + @Test + public void validateWhenNotBeforeIsSpecifiedThenReturnsSuccessfulResult() { Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.EXP)).build(); JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); From 36513ffef9f20afa27b59510c227a5fd81107d9a Mon Sep 17 00:00:00 2001 From: Ferenc Kemeny Date: Thu, 8 May 2025 22:41:36 +0200 Subject: [PATCH 2/2] Support Requiring exp and nbf in JwtTimestampsValidator Closes gh-17004 Signed-off-by: Ferenc Kemeny --- .../oauth2/jwt/JwtTimestampValidator.java | 45 +++++++++++++++---- .../jwt/JwtTimestampValidatorTests.java | 18 +++++++- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java index d191b8b11a6..f23dfbe4f76 100644 --- a/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java +++ b/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * An implementation of {@link OAuth2TokenValidator} for verifying claims in a Jwt-based @@ -54,6 +55,10 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator { private final Duration clockSkew; + private boolean allowEmptyExpiryClaim = true; + + private boolean allowEmptyNotBeforeClaim = true; + private Clock clock = Clock.systemUTC(); /** @@ -68,30 +73,54 @@ public JwtTimestampValidator(Duration clockSkew) { this.clockSkew = clockSkew; } + /** + * Whether to allow the {@code exp} header to be empty. The default value is + * {@code true} + * + * @since 7.0 + */ + public void setAllowEmptyExpiryClaim(boolean allowEmptyExpiryClaim) { + this.allowEmptyExpiryClaim = allowEmptyExpiryClaim; + } + + /** + * Whether to allow the {@code nbf} header to be empty. The default value is + * {@code true} + * + * @since 7.0 + */ + public void setAllowEmptyNotBeforeClaim(boolean allowEmptyNotBeforeClaim) { + this.allowEmptyNotBeforeClaim = allowEmptyNotBeforeClaim; + } + @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { Assert.notNull(jwt, "jwt cannot be null"); Instant expiry = jwt.getExpiresAt(); + if (!this.allowEmptyExpiryClaim && ObjectUtils.isEmpty(expiry)) { + return createOAuth2Error("exp is required"); + } if (expiry != null) { if (Instant.now(this.clock).minus(this.clockSkew).isAfter(expiry)) { - OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt())); - return OAuth2TokenValidatorResult.failure(oAuth2Error); + return createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt())); } } Instant notBefore = jwt.getNotBefore(); + if (!this.allowEmptyNotBeforeClaim && ObjectUtils.isEmpty(notBefore)) { + return createOAuth2Error("nbf is required"); + } if (notBefore != null) { if (Instant.now(this.clock).plus(this.clockSkew).isBefore(notBefore)) { - OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore())); - return OAuth2TokenValidatorResult.failure(oAuth2Error); + return createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore())); } } return OAuth2TokenValidatorResult.success(); } - private OAuth2Error createOAuth2Error(String reason) { + private OAuth2TokenValidatorResult createOAuth2Error(String reason) { this.logger.debug(reason); - return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason, - "https://tools.ietf.org/html/rfc6750#section-3.1"); + return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason, + "https://tools.ietf.org/html/rfc6750#section-3.1")); } /** diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java index 272004a5ec4..0d88794c0fe 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -158,6 +158,22 @@ public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSucces assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse(); } + @Test + public void validateWhenNotAllowEmptyExpiryClaimAndNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.EXP)).notBefore(Instant.MIN).build(); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + jwtValidator.setAllowEmptyExpiryClaim(false); + assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue(); + } + + @Test + public void validateWhenNotAllowEmptyNotBeforeClaimAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() { + Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.NBF)).build(); + JwtTimestampValidator jwtValidator = new JwtTimestampValidator(); + jwtValidator.setAllowEmptyNotBeforeClaim(false); + assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue(); + } + @Test public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() { Jwt jwt = TestJwts.jwt().build();