Skip to content

Commit

Permalink
Closes #816 (#836)
Browse files Browse the repository at this point in the history
Closes #816

Fixed exception message per recommendation.

Also updated expiration message to be clearer/intuitive.
  • Loading branch information
lhazlewood authored Sep 17, 2023
1 parent e9df2da commit a2b6576
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 99 deletions.
27 changes: 13 additions & 14 deletions impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -658,42 +658,41 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe
final Date now = this.clock.now();
long nowTime = now.getTime();

//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:
// https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4
// token MUST NOT be accepted on or after any specified exp time:
Date exp = claims.getExpiration();
if (exp != null) {

long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) {
String expVal = DateFormats.formatIso8601(exp, false);
String nowVal = DateFormats.formatIso8601(now, false);
String expVal = DateFormats.formatIso8601(exp, true);
String nowVal = DateFormats.formatIso8601(now, true);

long differenceMillis = nowTime - exp.getTime();

String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
differenceMillis + " milliseconds. Allowed clock skew: " +
String msg = "JWT expired " + differenceMillis + " milliseconds ago at " + expVal + ". " +
"Current time: " + nowVal + ". Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds.";
throw new ExpiredJwtException(header, claims, msg);
}
}

//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5
//token MUST NOT be accepted before any specified nbf time:
// https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5
// token MUST NOT be accepted before any specified nbf time:
Date nbf = claims.getNotBefore();
if (nbf != null) {

long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) {
String nbfVal = DateFormats.formatIso8601(nbf, false);
String nowVal = DateFormats.formatIso8601(now, false);
String nbfVal = DateFormats.formatIso8601(nbf, true);
String nowVal = DateFormats.formatIso8601(now, true);

long differenceMillis = nbf.getTime() - minTime;
long differenceMillis = nbf.getTime() - nowTime;

String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
", a difference of " +
differenceMillis + " milliseconds. Allowed clock skew: " +
String msg = "JWT early by " + differenceMillis + " milliseconds before " + nbfVal +
". Current time: " + nowVal + ". Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds.";
throw new PrematureJwtException(header, claims, msg);
}
Expand Down
164 changes: 83 additions & 81 deletions impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package io.jsonwebtoken

import io.jsonwebtoken.impl.DefaultClock

import io.jsonwebtoken.impl.DefaultJwtParser
import io.jsonwebtoken.impl.FixedClock
import io.jsonwebtoken.impl.JwtTokenizer
import io.jsonwebtoken.impl.lang.JwtDateConverter
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException
Expand Down Expand Up @@ -224,52 +226,80 @@ class JwtParserTest {
} catch (ExpiredJwtException e) {
// https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp):
// https://github.com/jwtk/jjwt/issues/660 (show differences as now - expired)
assertEquals e.getMessage(), "JWT expired at 2022-07-11T15:15:36Z. Current time: " +
"2022-07-11T15:15:37Z, a difference of 1573 milliseconds. Allowed clock skew: 0 milliseconds."
String msg = "JWT expired 1573 milliseconds ago at 2022-07-11T15:15:36.000Z. " +
"Current time: 2022-07-11T15:15:37.573Z. Allowed clock skew: 0 milliseconds."
assertEquals msg, e.message
}
}

@Test
void testParseWithPrematureJwt() {

Date nbf = new Date(System.currentTimeMillis() + 100000)
long differenceMillis = 100000 // arbitrary, anything > 0 is fine
def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def earlier = new Date(nbf.getTime() - differenceMillis)

String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact()
String compact = Jwts.builder().subject('Joe').notBefore(nbf).compact()

try {
Jwts.parser().enableUnsecured().build().parse(compact)
Jwts.parser().enableUnsecured().clock(new FixedClock(earlier)).build().parse(compact)
fail()
} catch (PrematureJwtException e) {
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
def nbf8601 = DateFormats.formatIso8601(nbf, true)
def earlier8601 = DateFormats.formatIso8601(earlier, true)
String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " +
"Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds.";
assertEquals msg, e.message

//https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp):
assertTrue e.getMessage().contains('Z, a difference of ')
assertTrue nbf8601.endsWith('Z')
assertTrue earlier8601.endsWith('Z')
}
}

@Test
void testParseWithExpiredJwtWithinAllowedClockSkew() {
Date exp = new Date(System.currentTimeMillis() - 3000)

long differenceMillis = 3000 // arbitrary, anything > 0 is fine
long millis = System.currentTimeMillis()
// RFC requires time in seconds, so we need to base our assertions based on second-normalized dates,
// otherwise we'll get nondeterministic tests:
long seconds = (millis / 1000L).longValue()
millis = seconds * 1000L
def exp = new Date(millis)
def later = new Date(exp.getTime() + differenceMillis)
def s = Jwts.builder().expiration(exp).compact()

String subject = 'Joe'
String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact()
String compact = Jwts.builder().subject(subject).expiration(exp).compact()

Jwt<Header, Claims> jwt = Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(10).build().parse(compact)
Jwt<Header, Claims> jwt = Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(10)
.clock(new FixedClock(later)).build().parse(compact)

assertEquals jwt.getPayload().getSubject(), subject
}

@Test
void testParseWithExpiredJwtNotWithinAllowedClockSkew() {
Date exp = new Date(System.currentTimeMillis() - 3000)

String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact()
long differenceMillis = 3000 // arbitrary, anything > 0 is fine
def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def later = new Date(exp.getTime() + differenceMillis)

def s = Jwts.builder().expiration(exp).compact()

def skewSeconds = 1

try {
Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(1).build().parse(compact)
Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(skewSeconds)
.clock(new FixedClock(later)).build().parse(s)
fail()
} catch (ExpiredJwtException e) {
assertTrue e.getMessage().startsWith('JWT expired at ')
def exp8601 = DateFormats.formatIso8601(exp, true)
def later8601 = DateFormats.formatIso8601(later, true)
String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " +
"Current time: ${later8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds.";
assertEquals msg, e.message
}
}

Expand All @@ -287,15 +317,26 @@ class JwtParserTest {

@Test
void testParseWithPrematureJwtNotWithinAllowedClockSkew() {
Date exp = new Date(System.currentTimeMillis() + 3000)

String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact()
long differenceMillis = 3000 // arbitrary, anything > 0 is fine
def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def earlier = new Date(nbf.getTime() - differenceMillis)

String compact = Jwts.builder().subject('Joe').notBefore(nbf).compact()

def skewSeconds = 1

try {
Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(1).build().parse(compact)
Jwts.parser().enableUnsecured()
.setAllowedClockSkewSeconds(skewSeconds).clock(new FixedClock(earlier))
.build().parse(compact)
fail()
} catch (PrematureJwtException e) {
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
def nbf8601 = DateFormats.formatIso8601(nbf, true)
def earlier8601 = DateFormats.formatIso8601(earlier, true)
String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " +
"Current time: ${earlier8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds.";
assertEquals msg, e.message
}
}

Expand Down Expand Up @@ -417,38 +458,6 @@ class JwtParserTest {
}
}

@Test
void testParseClaimsJwtWithExpiredJwt() {

long nowMillis = System.currentTimeMillis()
//some time in the past:
Date exp = new Date(nowMillis - 1000)

String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact()

try {
Jwts.parser().enableUnsecured().build().parseClaimsJwt(compact)
fail()
} catch (ExpiredJwtException e) {
assertTrue e.getMessage().startsWith('JWT expired at ')
}
}

@Test
void testParseClaimsJwtWithPrematureJwt() {

Date nbf = new Date(System.currentTimeMillis() + 100000)

String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact()

try {
Jwts.parser().enableUnsecured().build().parseClaimsJwt(compact)
fail()
} catch (PrematureJwtException e) {
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
}
}

// ========================================================================
// parseContentJws tests
// ========================================================================
Expand Down Expand Up @@ -542,21 +551,23 @@ class JwtParserTest {
@Test
void testParseClaimsJwsWithExpiredJws() {

String sub = 'Joe'
long differenceMillis = 843 // arbitrary, anything > 0 is fine
def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def later = new Date(exp.getTime() + differenceMillis)

String sub = 'Joe'
byte[] key = randomKey()

long nowMillis = System.currentTimeMillis()
//some time in the past:
Date exp = new Date(nowMillis - 1000)

String compact = Jwts.builder().setSubject(sub).signWith(SignatureAlgorithm.HS256, key).setExpiration(exp).compact()
String compact = Jwts.builder().subject(sub).expiration(exp).signWith(SignatureAlgorithm.HS256, key).compact()

try {
Jwts.parser().setSigningKey(key).build().parseClaimsJwt(compact)
Jwts.parser().setSigningKey(key).clock(new FixedClock(later)).build().parseClaimsJwt(compact)
fail()
} catch (ExpiredJwtException e) {
assertTrue e.getMessage().startsWith('JWT expired at ')
def exp8601 = DateFormats.formatIso8601(exp, true)
def later8601 = DateFormats.formatIso8601(later, true)
String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " +
"Current time: ${later8601}. Allowed clock skew: 0 milliseconds.";
assertEquals msg, e.message
assertEquals e.getClaims().getSubject(), sub
assertEquals e.getHeader().getAlgorithm(), "HS256"
}
Expand All @@ -565,19 +576,24 @@ class JwtParserTest {
@Test
void testParseClaimsJwsWithPrematureJws() {

String sub = 'Joe'
long differenceMillis = 3842 // arbitrary, anything > 0 is fine
def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def earlier = new Date(nbf.getTime() - differenceMillis)

String sub = 'Joe'
byte[] key = randomKey()

Date nbf = new Date(System.currentTimeMillis() + 100000)

String compact = Jwts.builder().setSubject(sub).setNotBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact()
String compact = Jwts.builder().subject(sub).notBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact()

try {
Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact)
Jwts.parser().setSigningKey(key).clock(new FixedClock(earlier)).build().parseClaimsJws(compact)
fail()
} catch (PrematureJwtException e) {
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
def nbf8601 = DateFormats.formatIso8601(nbf, true)
def earlier8601 = DateFormats.formatIso8601(earlier, true)
String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " +
"Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds.";
assertEquals msg, e.message

assertEquals e.getClaims().getSubject(), sub
assertEquals e.getHeader().getAlgorithm(), "HS256"
}
Expand Down Expand Up @@ -1545,20 +1561,6 @@ class JwtParserTest {
}
}

@Test
void testParseClockManipulationWithDefaultClock() {
Date expiry = new Date(System.currentTimeMillis() - 1000)

String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact()

try {
Jwts.parser().enableUnsecured().setClock(new DefaultClock()).build().parse(compact)
fail()
} catch (ExpiredJwtException e) {
assertTrue e.getMessage().startsWith('JWT expired at ')
}
}

@Test
void testParseMalformedJwt() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@
package io.jsonwebtoken.impl

import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.*
import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.JwtDateConverter
import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.impl.security.TestKeys
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Collections
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import org.junit.Before
Expand Down Expand Up @@ -251,4 +250,42 @@ class DefaultJwtParserTest {
assertEquals 'whatever', parsedCrit.iterator().next()
assertEquals 42, jwt.getHeader().get('whatever')
}

@Test
void testExpiredExceptionMessage() {

long differenceMillis = 843 // arbitrary, anything > 0 is fine
def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def later = new Date(exp.getTime() + differenceMillis)
def s = Jwts.builder().expiration(exp).compact()

try {
Jwts.parser().enableUnsecured().clock(new FixedClock(later)).build().parseClaimsJwt(s)
} catch (ExpiredJwtException expected) {
def exp8601 = DateFormats.formatIso8601(exp, true)
def later8601 = DateFormats.formatIso8601(later, true)
String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " +
"Current time: ${later8601}. Allowed clock skew: 0 milliseconds.";
assertEquals msg, expected.message
}
}

@Test
void testNotBeforeExceptionMessage() {

long differenceMillis = 3842 // arbitrary, anything > 0 is fine
def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L)
def earlier = new Date(nbf.getTime() - differenceMillis)
def s = Jwts.builder().notBefore(nbf).compact()

try {
Jwts.parser().enableUnsecured().clock(new FixedClock(earlier)).build().parseClaimsJwt(s)
} catch (PrematureJwtException expected) {
def nbf8601 = DateFormats.formatIso8601(nbf, true)
def earlier8601 = DateFormats.formatIso8601(earlier, true)
String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " +
"Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds.";
assertEquals msg, expected.message
}
}
}

0 comments on commit a2b6576

Please sign in to comment.