1212class JWTVerifier {
1313 private final Algorithm algorithm ;
1414 final Map <String , Object > claims ;
15+ private final Clock clock ;
1516
16- private JWTVerifier (Algorithm algorithm , Map <String , Object > claims ) {
17+ private JWTVerifier (Algorithm algorithm , Map <String , Object > claims , Clock clock ) {
1718 this .algorithm = algorithm ;
1819 this .claims = Collections .unmodifiableMap (claims );
20+ this .clock = clock ;
1921 }
2022
2123 /**
@@ -35,6 +37,7 @@ static JWTVerifier.Verification init(Algorithm algorithm) throws IllegalArgument
3537 static class Verification {
3638 private final Algorithm algorithm ;
3739 private final Map <String , Object > claims ;
40+ private long defaultDelta ;
3841
3942 Verification (Algorithm algorithm ) throws IllegalArgumentException {
4043 if (algorithm == null ) {
@@ -43,6 +46,7 @@ static class Verification {
4346
4447 this .algorithm = algorithm ;
4548 this .claims = new HashMap <>();
49+ this .defaultDelta = 0 ;
4650 }
4751
4852 /**
@@ -76,32 +80,66 @@ public Verification withAudience(String[] audience) {
7680 }
7781
7882 /**
79- * Require a specific Expires At ("exp") claim.
83+ * Define the default window in milliseconds in which the Not Before, Issued At and Expires At Claims will still be valid.
84+ * Setting a specific delta value on a given Claim will override this value for that Claim.
8085 *
86+ * @param delta the window in milliseconds in which the Not Before, Issued At and Expires At Claims will still be valid.
8187 * @return this same Verification instance.
88+ * @throws IllegalArgumentException if delta is negative.
8289 */
83- public Verification withExpiresAt (Date expiresAt ) {
84- requireClaim (PublicClaims .EXPIRES_AT , expiresAt );
90+ public Verification acceptTimeDelta (long delta ) throws IllegalArgumentException {
91+ if (delta < 0 ) {
92+ throw new IllegalArgumentException ("Delta value can't be negative." );
93+ }
94+ this .defaultDelta = delta ;
95+ return this ;
96+ }
97+
98+ /**
99+ * Set a specific delta window in milliseconds in which the Expires At ("exp") Claim will still be valid.
100+ * Expiration Date is always verified when the value is present. This method overrides the value set with acceptTimeDelta
101+ *
102+ * @param delta the window in milliseconds in which the Expires At Claim will still be valid.
103+ * @return this same Verification instance.
104+ * @throws IllegalArgumentException if delta is negative.
105+ */
106+ public Verification acceptExpiresAt (long delta ) throws IllegalArgumentException {
107+ if (delta < 0 ) {
108+ throw new IllegalArgumentException ("Delta value can't be negative." );
109+ }
110+ requireClaim (PublicClaims .EXPIRES_AT , delta );
85111 return this ;
86112 }
87113
88114 /**
89- * Require a specific Not Before ("nbf") claim.
115+ * Set a specific delta window in milliseconds in which the Not Before ("nbf") Claim will still be valid.
116+ * Not Before Date is always verified when the value is present. This method overrides the value set with acceptTimeDelta
90117 *
118+ * @param delta the window in milliseconds in which the Not Before Claim will still be valid.
91119 * @return this same Verification instance.
120+ * @throws IllegalArgumentException if delta is negative.
92121 */
93- public Verification withNotBefore (Date notBefore ) {
94- requireClaim (PublicClaims .NOT_BEFORE , notBefore );
122+ public Verification acceptNotBefore (long delta ) throws IllegalArgumentException {
123+ if (delta < 0 ) {
124+ throw new IllegalArgumentException ("Delta value can't be negative." );
125+ }
126+ requireClaim (PublicClaims .NOT_BEFORE , delta );
95127 return this ;
96128 }
97129
98130 /**
99- * Require a specific Issued At ("iat") claim.
131+ * Set a specific delta window in milliseconds in which the Issued At ("iat") Claim will still be valid.
132+ * Issued At Date is always verified when the value is present. This method overrides the value set with acceptTimeDelta
100133 *
134+ * @param delta the window in milliseconds in which the Issued At Claim will still be valid.
101135 * @return this same Verification instance.
136+ * @throws IllegalArgumentException if delta is negative.
102137 */
103- public Verification withIssuedAt (Date issuedAt ) {
104- requireClaim (PublicClaims .ISSUED_AT , issuedAt );
138+ public Verification acceptIssuedAt (long delta ) throws IllegalArgumentException {
139+ if (delta < 0 ) {
140+ throw new IllegalArgumentException ("Delta value can't be negative." );
141+ }
142+ requireClaim (PublicClaims .ISSUED_AT , delta );
105143 return this ;
106144 }
107145
@@ -121,7 +159,31 @@ public Verification withJWTId(String jwtId) {
121159 * @return a new JWTVerifier instance.
122160 */
123161 public JWTVerifier build () {
124- return new JWTVerifier (algorithm , claims );
162+ return this .build (new Clock ());
163+ }
164+
165+ /**
166+ * Creates a new and reusable instance of the JWTVerifier with the configuration already provided.
167+ * ONLY FOR TEST PURPOSES.
168+ *
169+ * @param clock the instance that will handle the current time.
170+ * @return a new JWTVerifier instance with a custom Clock.
171+ */
172+ JWTVerifier build (Clock clock ) {
173+ addDeltaToDateClaims ();
174+ return new JWTVerifier (algorithm , claims , clock );
175+ }
176+
177+ private void addDeltaToDateClaims () {
178+ if (!claims .containsKey (PublicClaims .EXPIRES_AT )) {
179+ claims .put (PublicClaims .EXPIRES_AT , defaultDelta );
180+ }
181+ if (!claims .containsKey (PublicClaims .NOT_BEFORE )) {
182+ claims .put (PublicClaims .NOT_BEFORE , defaultDelta );
183+ }
184+ if (!claims .containsKey (PublicClaims .ISSUED_AT )) {
185+ claims .put (PublicClaims .ISSUED_AT , defaultDelta );
186+ }
125187 }
126188
127189 private void requireClaim (String name , Object value ) {
@@ -167,19 +229,30 @@ private void verifyClaims(JWT jwt, Map<String, Object> claims) {
167229 }
168230
169231 private void assertValidClaim (JWT jwt , String claimName , Object expectedValue ) throws InvalidClaimException {
232+ String errMessage = String .format ("The Claim '%s' value doesn't match the required one." , claimName );
170233 boolean isValid ;
171234 if (PublicClaims .AUDIENCE .equals (claimName )) {
172235 isValid = Arrays .equals (jwt .getAudience (), (String []) expectedValue );
173236 } else if (PublicClaims .NOT_BEFORE .equals (claimName ) || PublicClaims .EXPIRES_AT .equals (claimName ) || PublicClaims .ISSUED_AT .equals (claimName )) {
174- Date dateValue = (Date ) expectedValue ;
175- isValid = dateValue .equals (jwt .getClaim (claimName ).asDate ());
237+ long deltaValue = (long ) expectedValue ;
238+ Date today = clock .getToday ();
239+ Date date = jwt .getClaim (claimName ).asDate ();
240+ if (PublicClaims .EXPIRES_AT .equals (claimName )) {
241+ today .setTime (today .getTime () - deltaValue );
242+ isValid = date == null || !today .after (date );
243+ errMessage = String .format ("The Token has expired on %s." , date );
244+ } else {
245+ today .setTime (today .getTime () + deltaValue );
246+ isValid = date == null || !today .before (date );
247+ errMessage = String .format ("The Token can't be used before %s." , date );
248+ }
176249 } else {
177250 String stringValue = (String ) expectedValue ;
178251 isValid = stringValue .equals (jwt .getClaim (claimName ).asString ());
179252 }
180253
181254 if (!isValid ) {
182- throw new InvalidClaimException (String . format ( "The Claim '%s' value doesn't match the required one." , claimName ) );
255+ throw new InvalidClaimException (errMessage );
183256 }
184257 }
185258}
0 commit comments