diff --git a/src/main/java/io/jsonwebtoken/Clock.java b/src/main/java/io/jsonwebtoken/Clock.java new file mode 100644 index 00000000..7e57df80 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/Clock.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken; + +import io.jsonwebtoken.impl.DefaultClock; + +import java.util.Date; + +/** + * A clock represents a time source that can be used when creating and verifying JWTs. + * + * @since 0.7.0 + */ +public interface Clock { + + public static final Clock DEFAULT = new DefaultClock(); + + /** + * Returns the clock's current timestamp at the instant the method is invoked. + * + * @return the clock's current timestamp at the instant the method is invoked. + */ + Date now(); +} diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 11e8a5f6..e0373093 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.impl.DefaultClock; + import java.security.Key; import java.util.Date; @@ -124,6 +126,16 @@ public interface JwtParser { */ JwtParser require(String claimName, Object value); + /** + * Sets the {@link Clock} that determines the timestamp to use when validating the parsed JWT. + * The parser uses a {@link DefaultClock DefaultClock} instance by default. + * + * @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT. + * @return the builder instance for method chaining. + * @since 0.7.0 + */ + JwtParser setClock(Clock clock); + /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClock.java b/src/main/java/io/jsonwebtoken/impl/DefaultClock.java new file mode 100644 index 00000000..14d1eed7 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClock.java @@ -0,0 +1,23 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Clock; + +import java.util.Date; + +/** + * Default {@link Clock} implementation. + * + * @since 0.7.0 + */ +public class DefaultClock implements Clock { + + /** + * Simply returns new {@link Date}(). + * + * @return a new {@link Date} instance. + */ + @Override + public Date now() { + return new Date(); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a5d68aec..8f8c02c8 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Clock; import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; @@ -69,6 +70,8 @@ public class DefaultJwtParser implements JwtParser { Claims expectedClaims = new DefaultClaims(); + private Clock clock = Clock.DEFAULT; + @Override public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); @@ -127,6 +130,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser setClock(Clock clock) { + Assert.notNull(clock, "Clock instance cannot be null."); + this.clock = clock; + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -346,16 +356,15 @@ public class DefaultJwtParser implements JwtParser { //since 0.3: if (claims != null) { - Date now = null; SimpleDateFormat sdf; + final Date now = this.clock.now(); + //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 //token MUST NOT be accepted on or after any specified exp time: Date exp = claims.getExpiration(); if (exp != null) { - now = new Date(); - if (now.equals(exp) || now.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = sdf.format(exp); @@ -371,10 +380,6 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - if (now == null) { - now = new Date(); - } - if (now.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = sdf.format(nbf); diff --git a/src/main/java/io/jsonwebtoken/impl/FixedClock.java b/src/main/java/io/jsonwebtoken/impl/FixedClock.java new file mode 100644 index 00000000..88de45dc --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/FixedClock.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl; + +import io.jsonwebtoken.Clock; + +import java.util.Date; + +/** + * A {@code Clock} implementation that is constructed with a seed timestamp and always reports that same + * timestamp. + * + * @since 0.7.0 + */ +public class FixedClock implements Clock { + + private final Date now; + + /** + * Creates a new fixed clock using new {@link Date Date}() as the seed timestamp. All calls to + * {@link #now now()} will always return this seed Date. + */ + public FixedClock() { + this(new Date()); + } + + /** + * Creates a new fixed clock using the specified seed timestamp. All calls to + * {@link #now now()} will always return this seed Date. + * + * @param now the specified Date to always return from all calls to {@link #now now()}. + */ + public FixedClock(Date now) { + this.now = now; + } + + @Override + public Date now() { + return this.now; + } +} diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 3e37658f..40dba0c7 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -15,6 +15,8 @@ */ package io.jsonwebtoken +import io.jsonwebtoken.impl.DefaultClock +import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.impl.TextCodec import org.junit.Test @@ -1392,4 +1394,39 @@ class JwtParserTest { ) } } + + @Test + void testParseClockManipulationWithFixedClock() { + def then = System.currentTimeMillis() - 1000 + Date expiry = new Date(then) + Date beforeExpiry = new Date(then - 1000) + + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() + + Jwts.parser().setClock(new FixedClock(beforeExpiry)).parse(compact) + } + + @Test + void testParseClockManipulationWithNullClock() { + JwtParser parser = Jwts.parser(); + try { + parser.setClock(null) + fail() + } catch (IllegalArgumentException expected) { + } + } + + @Test + void testParseClockManipulationWithDefaultClock() { + Date expiry = new Date(System.currentTimeMillis() - 1000) + + String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() + + try { + Jwts.parser().setClock(new DefaultClock()).parse(compact) + fail() + } catch (ExpiredJwtException e) { + assertTrue e.getMessage().startsWith('JWT expired at ') + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy new file mode 100644 index 00000000..fc092e6a --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl + +import org.junit.Test +import static org.junit.Assert.* + +class FixedClockTest { + + @Test + void testFixedClockDefaultConstructor() { + + def clock = new FixedClock() + + def date1 = clock.now() + Thread.sleep(100) + def date2 = clock.now() + + assertSame date1, date2 + } +}