Skip to content

Commit f526ea4

Browse files
authored
Merge pull request #16 from atholbro/support-unlimited-expiry-time
Add support for unlimited expiry tokens by leaving the expiry time null.
2 parents 29774de + 2b5e3d1 commit f526ea4

8 files changed

Lines changed: 153 additions & 20 deletions

File tree

paseto-core/src/main/java/net/aholbrook/paseto/claims/Claims.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ private Claims() {
1212
new IssuedInPast(), new CurrentlyValid()
1313
};
1414

15+
public static final Claim[] DEFAULT_NO_EXPIRY_CLAIM_CHECKS = new Claim[] {
16+
new IssuedInPast(), new CurrentlyValid(true)
17+
};
18+
1519
public static VerificationContext verify(Token token) {
1620
return verify(token, DEFAULT_CLAIM_CHECKS);
1721
}

paseto-core/src/main/java/net/aholbrook/paseto/claims/CurrentlyValid.java

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class CurrentlyValid implements Claim {
1818

1919
private final OffsetDateTime time;
2020
private final Duration allowableDrift;
21+
private final boolean allowWithoutExpiration;
2122

2223
/**
2324
* Verifies that the token is not expired or validated before it's "Not Before" time.
@@ -28,7 +29,20 @@ public class CurrentlyValid implements Claim {
2829
* relaxes the check by adding a 1 second window into the future during which the not before check will pass.
2930
*/
3031
public CurrentlyValid() {
31-
this(DEFAULT_ALLOWABLE_DRIFT);
32+
this(DEFAULT_ALLOWABLE_DRIFT, false);
33+
}
34+
35+
/**
36+
* Verifies that the token is not expired or validated before it's "Not Before" time.
37+
*
38+
* This call sets the "check time" to Clock.systemUTC() and should be used in most cases.
39+
*
40+
* This constructor sets the allowable clock drift as DEFAULT_ALLOWABLE_DRIFT which is defined as 1 second. This
41+
* relaxes the check by adding a 1 second window into the future during which the not before check will pass.
42+
* @param allowWithoutExpiration When true, treat tokens without a set expiry as valid.
43+
*/
44+
public CurrentlyValid(boolean allowWithoutExpiration) {
45+
this(DEFAULT_ALLOWABLE_DRIFT, allowWithoutExpiration);
3246
}
3347

3448
/**
@@ -42,7 +56,22 @@ public CurrentlyValid() {
4256
* time.
4357
*/
4458
public CurrentlyValid(Duration allowableDrift) {
45-
this(null, allowableDrift);
59+
this(null, allowableDrift, false);
60+
}
61+
62+
/**
63+
* Verifies that the token is not expired or validated before it's "Not Before" time.
64+
*
65+
* This call sets the "check time" to Clock.systemUTC() and should be used in most cases.
66+
*
67+
* @param allowableDrift Time window during which a token is considered valid even if it's not before time is in
68+
* the future. Should be set to a small time window (default is 1 second) which allows for a
69+
* slight clock drift between servers. Only applies to "not before" and not the expiration
70+
* time.
71+
* @param allowWithoutExpiration When true, treat tokens without a set expiry as valid.
72+
*/
73+
public CurrentlyValid(Duration allowableDrift, boolean allowWithoutExpiration) {
74+
this(null, allowableDrift, allowWithoutExpiration);
4675
}
4776

4877
/**
@@ -57,10 +86,12 @@ public CurrentlyValid(Duration allowableDrift) {
5786
* the future. Should be set to a small time window (default is 1 second) which allows for a
5887
* slight clock drift between servers. Only applies to "not before" and not the expiration
5988
* time.
89+
* @param allowWithoutExpiration When true, treat tokens without a set expiry as valid.
6090
*/
61-
public CurrentlyValid(OffsetDateTime time, Duration allowableDrift) {
91+
public CurrentlyValid(OffsetDateTime time, Duration allowableDrift, boolean allowWithoutExpiration) {
6292
this.time = time;
6393
this.allowableDrift = allowableDrift;
94+
this.allowWithoutExpiration = allowWithoutExpiration;
6495
}
6596

6697
@Override
@@ -70,15 +101,9 @@ public String name() {
70101

71102
@Override
72103
public void check(Token token, VerificationContext context) {
104+
// Note: issued at times can be checked with the IssuedInPast rule.
73105
OffsetDateTime time = this.time == null ? OffsetDateTime.now(Clock.systemUTC()) : this.time;
74106

75-
// If no expiry time was set, then we treat the token as expired.
76-
if (token.getExpiration() == null) {
77-
throw new MissingClaimException(Token.CLAIM_EXPIRATION, NAME, token);
78-
}
79-
80-
OffsetDateTime expiration = Instant.ofEpochSecond(token.getExpiration()).atOffset(ZoneOffset.UTC);
81-
82107
// Check "Not Before" if provided.
83108
if (token.getNotBefore() != null) {
84109
OffsetDateTime notBefore = Instant.ofEpochSecond(token.getNotBefore()).atOffset(ZoneOffset.UTC);
@@ -88,7 +113,16 @@ public void check(Token token, VerificationContext context) {
88113
}
89114
}
90115

91-
// Note: issued at times can be checked with the IssuedInPast rule.
116+
// If no expiry time was set, then we treat the token as expired unless allowWithoutExpiration is true.
117+
if (token.getExpiration() == null) {
118+
if (allowWithoutExpiration) {
119+
return; // valid
120+
} else {
121+
throw new MissingClaimException(Token.CLAIM_EXPIRATION, NAME, token);
122+
}
123+
}
124+
125+
OffsetDateTime expiration = Instant.ofEpochSecond(token.getExpiration()).atOffset(ZoneOffset.UTC);
92126

93127
// Finally we check the expiration time.
94128
if (expiration.isBefore(time)) {

paseto-core/src/main/java/net/aholbrook/paseto/service/LocalTokenService.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.aholbrook.paseto.Paseto;
44
import net.aholbrook.paseto.PasetoV1;
55
import net.aholbrook.paseto.PasetoV2;
6+
import net.aholbrook.paseto.PasetoV4;
67
import net.aholbrook.paseto.TokenWithFooter;
78
import net.aholbrook.paseto.claims.Claim;
89
import net.aholbrook.paseto.claims.Claims;
@@ -14,8 +15,8 @@ public class LocalTokenService<_TokenType extends Token> extends TokenService<_T
1415
private final KeyProvider keyProvider;
1516

1617
private LocalTokenService(Paseto paseto, KeyProvider keyProvider, Claim[] claims,
17-
Duration defaultValidityPeriod, Class<_TokenType> tokenClass) {
18-
super(paseto, claims, defaultValidityPeriod, tokenClass);
18+
Duration defaultValidityPeriod, Class<_TokenType> tokenClass, boolean allowTokensWithoutExpiration) {
19+
super(paseto, claims, defaultValidityPeriod, tokenClass, allowTokensWithoutExpiration);
1920
this.keyProvider = keyProvider;
2021
}
2122

@@ -107,6 +108,7 @@ public static class Builder<_TokenType extends Token> {
107108
private Paseto paseto;
108109
private Long defaultValidityPeriod = null;
109110
private Claim[] claims = Claims.DEFAULT_CLAIM_CHECKS;
111+
private boolean allowTokensWithoutExpiration = false;
110112

111113
public Builder(Class<_TokenType> tokenClass, KeyProvider keyProvider) {
112114
this.tokenClass = tokenClass;
@@ -123,6 +125,11 @@ public Builder<_TokenType> withV2() {
123125
return this;
124126
}
125127

128+
public Builder<_TokenType> withV4() {
129+
this.paseto = new PasetoV4.Builder().build();
130+
return this;
131+
}
132+
126133
public Builder<_TokenType> withPaseto(Paseto paseto) {
127134
this.paseto = paseto;
128135
return this;
@@ -133,6 +140,16 @@ public Builder<_TokenType> withDefaultValidityPeriod(Long seconds) {
133140
return this;
134141
}
135142

143+
public Builder<_TokenType> withoutExpiration() {
144+
this.allowTokensWithoutExpiration = true;
145+
146+
if (claims == Claims.DEFAULT_CLAIM_CHECKS) {
147+
claims = Claims.DEFAULT_NO_EXPIRY_CLAIM_CHECKS;
148+
}
149+
150+
return this;
151+
}
152+
136153
public Builder<_TokenType> checkClaims(Claim[] claims) {
137154
this.claims = claims;
138155
return this;
@@ -145,7 +162,8 @@ public LocalTokenService<_TokenType> build() {
145162
keyProvider,
146163
claims,
147164
defaultValidityPeriod != null ? Duration.ofSeconds(defaultValidityPeriod) : null,
148-
tokenClass);
165+
tokenClass,
166+
allowTokensWithoutExpiration);
149167
}
150168
}
151169
}

paseto-core/src/main/java/net/aholbrook/paseto/service/PublicTokenService.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.aholbrook.paseto.Paseto;
44
import net.aholbrook.paseto.PasetoV1;
55
import net.aholbrook.paseto.PasetoV2;
6+
import net.aholbrook.paseto.PasetoV4;
67
import net.aholbrook.paseto.TokenWithFooter;
78
import net.aholbrook.paseto.claims.Claim;
89
import net.aholbrook.paseto.claims.Claims;
@@ -16,8 +17,8 @@ public class PublicTokenService<_TokenType extends Token> extends TokenService<_
1617
private final KeyProvider keyProvider;
1718

1819
private PublicTokenService(Paseto paseto, KeyProvider keyProvider, Claim[] claims,
19-
Duration defaultValidityPeriod, Class<_TokenType> tokenClass) {
20-
super(paseto, claims, defaultValidityPeriod, tokenClass);
20+
Duration defaultValidityPeriod, Class<_TokenType> tokenClass, boolean allowTokensWithoutExpiration) {
21+
super(paseto, claims, defaultValidityPeriod, tokenClass, allowTokensWithoutExpiration);
2122
this.keyProvider = keyProvider;
2223
}
2324

@@ -110,6 +111,7 @@ public static class Builder<_TokenType extends Token> {
110111
private Paseto paseto;
111112
private Long defaultValidityPeriod = null;
112113
private Claim[] claims = Claims.DEFAULT_CLAIM_CHECKS;
114+
private boolean allowTokensWithoutExpiration = false;
113115

114116
public Builder(Class<_TokenType> tokenClass, KeyProvider keyProvider) {
115117
this.tokenClass = tokenClass;
@@ -126,6 +128,11 @@ public Builder<_TokenType> withV2() {
126128
return this;
127129
}
128130

131+
public Builder<_TokenType> withV4() {
132+
this.paseto = new PasetoV4.Builder().build();
133+
return this;
134+
}
135+
129136
public Builder<_TokenType> withPaseto(Paseto paseto) {
130137
this.paseto = paseto;
131138
return this;
@@ -136,6 +143,16 @@ public Builder<_TokenType> withDefaultValidityPeriod(Long defaultValidityPeriod)
136143
return this;
137144
}
138145

146+
public Builder<_TokenType> withoutExpiration() {
147+
this.allowTokensWithoutExpiration = true;
148+
149+
if (claims == Claims.DEFAULT_CLAIM_CHECKS) {
150+
claims = Claims.DEFAULT_NO_EXPIRY_CLAIM_CHECKS;
151+
}
152+
153+
return this;
154+
}
155+
139156
public Builder<_TokenType> checkClaims(Claim[] claims) {
140157
this.claims = claims;
141158
return this;
@@ -148,7 +165,8 @@ public PublicTokenService<_TokenType> build() {
148165
keyProvider,
149166
claims,
150167
defaultValidityPeriod != null ? Duration.ofSeconds(defaultValidityPeriod) : null,
151-
tokenClass);
168+
tokenClass,
169+
allowTokensWithoutExpiration);
152170
}
153171
}
154172
}

paseto-core/src/main/java/net/aholbrook/paseto/service/TokenService.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ public abstract class TokenService<_TokenType extends Token> {
1616
final Class<_TokenType> tokenClass;
1717
final Claim[] claims;
1818
private final Duration defaultValidityPeriod;
19+
private final boolean allowTokensWithoutExpiration;
1920

2021
TokenService(Paseto paseto, Claim[] claims, Duration defaultValidityPeriod,
21-
Class<_TokenType> tokenClass) {
22+
Class<_TokenType> tokenClass, boolean allowTokensWithoutExpiration) {
2223
this.paseto = paseto;
2324
this.tokenClass = tokenClass;
2425
this.defaultValidityPeriod = defaultValidityPeriod;
2526
this.claims = claims;
27+
this.allowTokensWithoutExpiration = allowTokensWithoutExpiration;
2628
}
2729

2830
abstract public String encode(_TokenType token);
@@ -62,7 +64,7 @@ protected final void validateToken(_TokenType token) {
6264
if (defaultValidityPeriod != null) {
6365
OffsetDateTime issuedAt = Instant.ofEpochSecond(token.getIssuedAt()).atOffset(ZoneOffset.UTC);
6466
token.setExpiration(issuedAt.plus(defaultValidityPeriod).toEpochSecond());
65-
} else {
67+
} else if (!allowTokensWithoutExpiration) {
6668
throw new MissingClaimException(Token.CLAIM_EXPIRATION, "TokenService", token);
6769
}
6870
}

paseto-core/src/test/java/net/aholbrook/paseto/ClaimVerificationTest.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private VerificationContext defaultVerification(Token token) {
3838
private VerificationContext standardVerification(Token token, OffsetDateTime time) {
3939
Claim[] claims = new Claim[] {
4040
new IssuedInPast(time, IssuedInPast.DEFAULT_ALLOWABLE_DRIFT),
41-
new CurrentlyValid(time, CurrentlyValid.DEFAULT_ALLOWABLE_DRIFT)
41+
new CurrentlyValid(time, CurrentlyValid.DEFAULT_ALLOWABLE_DRIFT, false)
4242
};
4343

4444
return Claims.verify(token, claims);
@@ -111,6 +111,19 @@ public void tokenVerification_expired_missing() {
111111
});
112112
}
113113

114+
@Test
115+
@DisplayName("A token without an expiration time is valid if allowWithoutExpiration is set to true.")
116+
public void tokenVerification_null_expired_allowed() {
117+
Token token = new Token().setIssuedAt(0L);
118+
119+
Claim[] claims = new Claim[] {
120+
new IssuedInPast(IssuedInPast.DEFAULT_ALLOWABLE_DRIFT),
121+
new CurrentlyValid(CurrentlyValid.DEFAULT_ALLOWABLE_DRIFT, true)
122+
};
123+
124+
Claims.verify(token, claims);
125+
}
126+
114127
@Test
115128
@DisplayName("CurrentlyValid claim name is correct.")
116129
public void tokenVerification_expired_name() {

paseto-core/src/test/java/net/aholbrook/paseto/PasetoV1ServiceTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,20 @@ public void v1Service_public_noExpiry(Paseto.Builder builder) {
317317
});
318318
}
319319

320+
@ParameterizedTest(name = "{displayName} with {0}")
321+
@MethodSource("net.aholbrook.paseto.Sources#pasetoV1Builders")
322+
public void v1Service_public_noExpiryAllowed(Paseto.Builder builder) {
323+
Token token = new Token().setTokenId("id").setIssuedAt(0L);
324+
PublicTokenService<Token> service = new PublicTokenService.Builder<>(Token.class, rfcPublicKeyProvider())
325+
.withPaseto(builder.build())
326+
.withoutExpiration()
327+
.build();
328+
329+
String s = service.encode(token);
330+
Token token2 = service.decode(s);
331+
Assertions.assertNotNull(token2.getIssuedAt());
332+
Assertions.assertNull(token2.getExpiration());
333+
}
320334

321335
// Constructors / Builder options
322336
@ParameterizedTest(name = "{displayName} with {0}")

paseto-core/src/test/java/net/aholbrook/paseto/PasetoV2ServiceTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,21 @@ public void v2Service_local_noExpiry(Paseto.Builder builder) {
308308
});
309309
}
310310

311+
@ParameterizedTest(name = "{displayName} with {0}")
312+
@MethodSource("net.aholbrook.paseto.Sources#pasetoV2Builders")
313+
public void v2Service_local_noExpiryAllowed(Paseto.Builder builder) {
314+
Token token = new Token().setTokenId("id").setIssuedAt(0L);
315+
LocalTokenService<Token> service = new LocalTokenService.Builder<>(Token.class, rfcLocalKeyProvider())
316+
.withPaseto(builder.build())
317+
.withoutExpiration()
318+
.build();
319+
320+
String s = service.encode(token);
321+
Token token2 = service.decode(s);
322+
Assertions.assertNotNull(token2.getIssuedAt());
323+
Assertions.assertNull(token2.getExpiration());
324+
}
325+
311326
@ParameterizedTest(name = "{displayName} with {0}")
312327
@MethodSource("net.aholbrook.paseto.Sources#pasetoV2Builders")
313328
public void v2Service_public_defaultValidityPeriod(Paseto.Builder builder) {
@@ -337,6 +352,21 @@ public void v2Service_public_noExpiry(Paseto.Builder builder) {
337352
});
338353
}
339354

355+
@ParameterizedTest(name = "{displayName} with {0}")
356+
@MethodSource("net.aholbrook.paseto.Sources#pasetoV2Builders")
357+
public void v2Service_public_noExpiryAllowed(Paseto.Builder builder) {
358+
Token token = new Token().setTokenId("id").setIssuedAt(0L);
359+
PublicTokenService<Token> service = new PublicTokenService.Builder<>(Token.class, rfcPublicKeyProvider())
360+
.withPaseto(builder.build())
361+
.withoutExpiration()
362+
.build();
363+
364+
String s = service.encode(token);
365+
Token token2 = service.decode(s);
366+
Assertions.assertNotNull(token2.getIssuedAt());
367+
Assertions.assertNull(token2.getExpiration());
368+
}
369+
340370

341371
// Constructors / Builder options
342372
@ParameterizedTest(name = "{displayName} with {0}")

0 commit comments

Comments
 (0)