Skip to content

Commit 36513ff

Browse files
FerencKemenyjzheaux
authored andcommitted
Support Requiring exp and nbf in JwtTimestampsValidator
Closes gh-17004 Signed-off-by: Ferenc Kemeny <[email protected]>
1 parent 5821cff commit 36513ff

File tree

2 files changed

+54
-9
lines changed

2 files changed

+54
-9
lines changed

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtTimestampValidator.java

+37-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@
2929
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
3030
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
3131
import org.springframework.util.Assert;
32+
import org.springframework.util.ObjectUtils;
3233

3334
/**
3435
* An implementation of {@link OAuth2TokenValidator} for verifying claims in a Jwt-based
@@ -54,6 +55,10 @@ public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
5455

5556
private final Duration clockSkew;
5657

58+
private boolean allowEmptyExpiryClaim = true;
59+
60+
private boolean allowEmptyNotBeforeClaim = true;
61+
5762
private Clock clock = Clock.systemUTC();
5863

5964
/**
@@ -68,30 +73,54 @@ public JwtTimestampValidator(Duration clockSkew) {
6873
this.clockSkew = clockSkew;
6974
}
7075

76+
/**
77+
* Whether to allow the {@code exp} header to be empty. The default value is
78+
* {@code true}
79+
*
80+
* @since 7.0
81+
*/
82+
public void setAllowEmptyExpiryClaim(boolean allowEmptyExpiryClaim) {
83+
this.allowEmptyExpiryClaim = allowEmptyExpiryClaim;
84+
}
85+
86+
/**
87+
* Whether to allow the {@code nbf} header to be empty. The default value is
88+
* {@code true}
89+
*
90+
* @since 7.0
91+
*/
92+
public void setAllowEmptyNotBeforeClaim(boolean allowEmptyNotBeforeClaim) {
93+
this.allowEmptyNotBeforeClaim = allowEmptyNotBeforeClaim;
94+
}
95+
7196
@Override
7297
public OAuth2TokenValidatorResult validate(Jwt jwt) {
7398
Assert.notNull(jwt, "jwt cannot be null");
7499
Instant expiry = jwt.getExpiresAt();
100+
if (!this.allowEmptyExpiryClaim && ObjectUtils.isEmpty(expiry)) {
101+
return createOAuth2Error("exp is required");
102+
}
75103
if (expiry != null) {
76104
if (Instant.now(this.clock).minus(this.clockSkew).isAfter(expiry)) {
77-
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt()));
78-
return OAuth2TokenValidatorResult.failure(oAuth2Error);
105+
return createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt()));
79106
}
80107
}
81108
Instant notBefore = jwt.getNotBefore();
109+
if (!this.allowEmptyNotBeforeClaim && ObjectUtils.isEmpty(notBefore)) {
110+
return createOAuth2Error("nbf is required");
111+
}
82112
if (notBefore != null) {
83113
if (Instant.now(this.clock).plus(this.clockSkew).isBefore(notBefore)) {
84-
OAuth2Error oAuth2Error = createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore()));
85-
return OAuth2TokenValidatorResult.failure(oAuth2Error);
114+
return createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore()));
86115
}
87116
}
88117
return OAuth2TokenValidatorResult.success();
89118
}
90119

91-
private OAuth2Error createOAuth2Error(String reason) {
120+
private OAuth2TokenValidatorResult createOAuth2Error(String reason) {
92121
this.logger.debug(reason);
93-
return new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason,
94-
"https://tools.ietf.org/html/rfc6750#section-3.1");
122+
return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, reason,
123+
"https://tools.ietf.org/html/rfc6750#section-3.1"));
95124
}
96125

97126
/**

oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtTimestampValidatorTests.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -158,6 +158,22 @@ public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSucces
158158
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
159159
}
160160

161+
@Test
162+
public void validateWhenNotAllowEmptyExpiryClaimAndNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() {
163+
Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.EXP)).notBefore(Instant.MIN).build();
164+
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
165+
jwtValidator.setAllowEmptyExpiryClaim(false);
166+
assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
167+
}
168+
169+
@Test
170+
public void validateWhenNotAllowEmptyNotBeforeClaimAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() {
171+
Jwt jwt = TestJwts.jwt().claims((c) -> c.remove(JwtClaimNames.NBF)).build();
172+
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
173+
jwtValidator.setAllowEmptyNotBeforeClaim(false);
174+
assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
175+
}
176+
161177
@Test
162178
public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() {
163179
Jwt jwt = TestJwts.jwt().build();

0 commit comments

Comments
 (0)