فهرست منبع

Support Requiring exp and nbf in JwtTimestampsValidator

Closes gh-17004

Signed-off-by: Ferenc Kemeny <ferenc.kemeny79+oss@gmail.com>
Ferenc Kemeny 3 ماه پیش
والد
کامیت
bf05b8b430

+ 37 - 8
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.OAuth2ErrorCodes;
 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<Jwt> {
 
 	private final Duration clockSkew;
 
+	private boolean allowEmptyExpiryClaim = true;
+
+	private boolean allowEmptyNotBeforeClaim = true;
+
 	private Clock clock = Clock.systemUTC();
 
 	/**
@@ -68,30 +73,54 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
 		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"));
 	}
 
 	/**

+ 17 - 1
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 class JwtTimestampValidatorTests {
 		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();