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
+ }
+}