Skip to content

Commit de7f6b2

Browse files
authored
Merge pull request auth0#94 from auth0/verify-time-values
Add time Claims verification with the isExpired method.
2 parents c4fffb2 + 980204a commit de7f6b2

12 files changed

Lines changed: 475 additions & 137 deletions

File tree

README.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,33 @@ If the token has an invalid syntax or the header or payload are not JSONs, a `JW
8787
If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise.
8888

8989

90+
#### Time Validation
91+
92+
The JWT token may include DateNumber fields that can be used to validate that:
93+
* The token was issued in a past date `"iat" < TODAY`
94+
* The token hasn't expired yet `"exp" > TODAY` and
95+
* The token can already be used. `"nbf" > TODAY`
96+
97+
When verifying a token the time validation occurs automatically, resulting in a `JWTVerificationException` being throw when the values are invalid. If any of the previous fields are missing they won't be considered in this validation.
98+
99+
To specify a **delta window** or leeway in which the Token should still be considered valid, use the `acceptTimeDelta()` method in the `JWTVerifier` builder and pass a positive milliseconds value. This applies to every item listed above.
100+
101+
```java
102+
JWTVerifier verifier = JWT.require(Algorithm.RSA256(key))
103+
.acceptTimeDelta(100) //nbf, iat and exp
104+
.build();
105+
```
106+
107+
You can also specify a custom value for a given Date claim and override the default one for only that claim.
108+
109+
```java
110+
JWTVerifier verifier = JWT.require(Algorithm.RSA256(key))
111+
.acceptTimeDelta(100) //nbf and iat
112+
.acceptExpiresAt(500) //exp
113+
.build();
114+
```
115+
116+
90117
### Registered Claims
91118

92119
#### Issuer ("iss")
@@ -145,14 +172,6 @@ Returns the JWT ID value or null if it's not defined.
145172
String id = jwt.getId();
146173
```
147174

148-
#### Time Validation
149-
150-
The JWT token may include DateNumber fields that can be used to validate that the token was issued in a past date `"iat" < TODAY` and that the expiration date is in the future `"exp" > TODAY`. This library includes a method that checks both of this fields and returns the validity of the token. If any of the fields is missing they won't be considered.
151-
152-
```java
153-
boolean isExpired = jwt.isExpired();
154-
```
155-
156175
### Private Claims
157176

158177
Additional Claims defined in the token can be obtained by calling `getClaim()` and passing the Claim name. A Claim will always be returned, even if it can't be found.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.auth0.jwt;
2+
3+
import java.util.Date;
4+
5+
/**
6+
* The Clock class is used to wrap calls to Date class.
7+
*/
8+
class Clock {
9+
10+
Clock() {
11+
}
12+
13+
/**
14+
* Returns a new Date representing Today's time.
15+
*
16+
* @return a new Date representing Today's time.
17+
*/
18+
Date getToday() {
19+
return new Date();
20+
}
21+
}

lib/src/main/java/com/auth0/jwt/JWT.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ public static JWTVerifier.Verification require(Algorithm algorithm) throws Illeg
3737
return JWTVerifier.init(algorithm);
3838
}
3939

40-
@Override
41-
public boolean isExpired() {
42-
return jwt.isExpired();
43-
}
44-
4540
@Override
4641
public String getSignature() {
4742
return jwt.getSignature();

lib/src/main/java/com/auth0/jwt/JWTDecoder.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,4 @@ public String getSignature() {
110110
return signature;
111111
}
112112

113-
@Override
114-
public boolean isExpired() {
115-
//TODO: Add advanced validation
116-
return false;
117-
}
118113
}

lib/src/main/java/com/auth0/jwt/JWTVerifier.java

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
class 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
}

lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ String[] getStringOrArray(Map<String, JsonNode> tree, String claimName) throws J
6464
return arr;
6565
}
6666

67-
private Date getDate(Map<String, JsonNode> tree, String claimName) {
67+
Date getDate(Map<String, JsonNode> tree, String claimName) {
6868
JsonNode node = tree.get(claimName);
6969
if (node == null || node.isNull() || !node.canConvertToLong()) {
7070
return null;
@@ -73,7 +73,7 @@ private Date getDate(Map<String, JsonNode> tree, String claimName) {
7373
return new Date(ms);
7474
}
7575

76-
private String getString(Map<String, JsonNode> tree, String claimName) {
76+
String getString(Map<String, JsonNode> tree, String claimName) {
7777
JsonNode node = tree.get(claimName);
7878
if (node == null || node.isNull()) {
7979
return null;

lib/src/main/java/com/auth0/jwt/interfaces/JWT.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,4 @@
44
* The JWT class represents a Json Web Token.
55
*/
66
public interface JWT extends Payload, Header, Signature {
7-
8-
//TODO replace with advanced validations
9-
boolean isExpired();
107
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.auth0.jwt;
2+
3+
import org.junit.Test;
4+
5+
import java.util.Date;
6+
7+
import static org.hamcrest.Matchers.equalTo;
8+
import static org.hamcrest.Matchers.is;
9+
import static org.hamcrest.Matchers.notNullValue;
10+
import static org.junit.Assert.*;
11+
12+
public class ClockTest {
13+
14+
@Test
15+
public void shouldGetToday() throws Exception{
16+
Clock clock = new Clock();
17+
Date clockToday = clock.getToday();
18+
Date today = new Date();
19+
20+
assertThat(clockToday, is(notNullValue()));
21+
assertThat(clockToday.getTime(), is(equalTo(today.getTime())));
22+
}
23+
24+
}

0 commit comments

Comments
 (0)