Skip to content

JwtTimestampsValidator can require exp and nbf claims #17030

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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();

/**
Expand All @@ -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"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -141,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();
Expand Down