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.
2929import org .springframework .security .oauth2 .core .OAuth2TokenValidator ;
3030import org .springframework .security .oauth2 .core .OAuth2TokenValidatorResult ;
3131import 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 /**
0 commit comments