Closes #816

Fixed exception message per recommendation.

Also updated expiration message to be clearer/intuitive.
This commit is contained in:
lhazlewood 2023-09-16 18:47:33 -07:00 committed by GitHub
parent e9df2da272
commit a2b65763e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 99 deletions

View File

@ -658,42 +658,41 @@ public class DefaultJwtParser implements JwtParser {
final Date now = this.clock.now(); final Date now = this.clock.now();
long nowTime = now.getTime(); long nowTime = now.getTime();
//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 // https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4
//token MUST NOT be accepted on or after any specified exp time: // token MUST NOT be accepted on or after any specified exp time:
Date exp = claims.getExpiration(); Date exp = claims.getExpiration();
if (exp != null) { if (exp != null) {
long maxTime = nowTime - this.allowedClockSkewMillis; long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now; Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) { if (max.after(exp)) {
String expVal = DateFormats.formatIso8601(exp, false); String expVal = DateFormats.formatIso8601(exp, true);
String nowVal = DateFormats.formatIso8601(now, false); String nowVal = DateFormats.formatIso8601(now, true);
long differenceMillis = nowTime - exp.getTime(); long differenceMillis = nowTime - exp.getTime();
String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + String msg = "JWT expired " + differenceMillis + " milliseconds ago at " + expVal + ". " +
differenceMillis + " milliseconds. Allowed clock skew: " + "Current time: " + nowVal + ". Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds."; this.allowedClockSkewMillis + " milliseconds.";
throw new ExpiredJwtException(header, claims, msg); throw new ExpiredJwtException(header, claims, msg);
} }
} }
//https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.5 // https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5
//token MUST NOT be accepted before any specified nbf time: // token MUST NOT be accepted before any specified nbf time:
Date nbf = claims.getNotBefore(); Date nbf = claims.getNotBefore();
if (nbf != null) { if (nbf != null) {
long minTime = nowTime + this.allowedClockSkewMillis; long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now; Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) { if (min.before(nbf)) {
String nbfVal = DateFormats.formatIso8601(nbf, false); String nbfVal = DateFormats.formatIso8601(nbf, true);
String nowVal = DateFormats.formatIso8601(now, false); 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 + String msg = "JWT early by " + differenceMillis + " milliseconds before " + nbfVal +
", a difference of " + ". Current time: " + nowVal + ". Allowed clock skew: " +
differenceMillis + " milliseconds. Allowed clock skew: " +
this.allowedClockSkewMillis + " milliseconds."; this.allowedClockSkewMillis + " milliseconds.";
throw new PrematureJwtException(header, claims, msg); throw new PrematureJwtException(header, claims, msg);
} }

View File

@ -15,11 +15,13 @@
*/ */
package io.jsonwebtoken package io.jsonwebtoken
import io.jsonwebtoken.impl.DefaultClock
import io.jsonwebtoken.impl.DefaultJwtParser import io.jsonwebtoken.impl.DefaultJwtParser
import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.impl.FixedClock
import io.jsonwebtoken.impl.JwtTokenizer import io.jsonwebtoken.impl.JwtTokenizer
import io.jsonwebtoken.impl.lang.JwtDateConverter
import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException import io.jsonwebtoken.security.SignatureException
@ -224,52 +226,80 @@ class JwtParserTest {
} catch (ExpiredJwtException e) { } 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/107 (the Z designator at the end of the timestamp):
// https://github.com/jwtk/jjwt/issues/660 (show differences as now - expired) // 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: " + String msg = "JWT expired 1573 milliseconds ago at 2022-07-11T15:15:36.000Z. " +
"2022-07-11T15:15:37Z, a difference of 1573 milliseconds. Allowed clock skew: 0 milliseconds." "Current time: 2022-07-11T15:15:37.573Z. Allowed clock skew: 0 milliseconds."
assertEquals msg, e.message
} }
} }
@Test @Test
void testParseWithPrematureJwt() { 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 { try {
Jwts.parser().enableUnsecured().build().parse(compact) Jwts.parser().enableUnsecured().clock(new FixedClock(earlier)).build().parse(compact)
fail() fail()
} catch (PrematureJwtException e) { } 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): //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 @Test
void testParseWithExpiredJwtWithinAllowedClockSkew() { 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 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 assertEquals jwt.getPayload().getSubject(), subject
} }
@Test @Test
void testParseWithExpiredJwtNotWithinAllowedClockSkew() { 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 { try {
Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(1).build().parse(compact) Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(skewSeconds)
.clock(new FixedClock(later)).build().parse(s)
fail() fail()
} catch (ExpiredJwtException e) { } 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
} }
} }
@ -287,15 +317,26 @@ class JwtParserTest {
@Test @Test
void testParseWithPrematureJwtNotWithinAllowedClockSkew() { 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 { try {
Jwts.parser().enableUnsecured().setAllowedClockSkewSeconds(1).build().parse(compact) Jwts.parser().enableUnsecured()
.setAllowedClockSkewSeconds(skewSeconds).clock(new FixedClock(earlier))
.build().parse(compact)
fail() fail()
} catch (PrematureJwtException e) { } 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
} }
} }
@ -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 // parseContentJws tests
// ======================================================================== // ========================================================================
@ -542,21 +551,23 @@ class JwtParserTest {
@Test @Test
void testParseClaimsJwsWithExpiredJws() { void testParseClaimsJwsWithExpiredJws() {
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' String sub = 'Joe'
byte[] key = randomKey() byte[] key = randomKey()
String compact = Jwts.builder().subject(sub).expiration(exp).signWith(SignatureAlgorithm.HS256, key).compact()
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()
try { try {
Jwts.parser().setSigningKey(key).build().parseClaimsJwt(compact) Jwts.parser().setSigningKey(key).clock(new FixedClock(later)).build().parseClaimsJwt(compact)
fail() fail()
} catch (ExpiredJwtException e) { } 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.getClaims().getSubject(), sub
assertEquals e.getHeader().getAlgorithm(), "HS256" assertEquals e.getHeader().getAlgorithm(), "HS256"
} }
@ -565,19 +576,24 @@ class JwtParserTest {
@Test @Test
void testParseClaimsJwsWithPrematureJws() { void testParseClaimsJwsWithPrematureJws() {
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' String sub = 'Joe'
byte[] key = randomKey() byte[] key = randomKey()
String compact = Jwts.builder().subject(sub).notBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact()
Date nbf = new Date(System.currentTimeMillis() + 100000)
String compact = Jwts.builder().setSubject(sub).setNotBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact()
try { try {
Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact) Jwts.parser().setSigningKey(key).clock(new FixedClock(earlier)).build().parseClaimsJws(compact)
fail() fail()
} catch (PrematureJwtException e) { } 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.getClaims().getSubject(), sub
assertEquals e.getHeader().getAlgorithm(), "HS256" assertEquals e.getHeader().getAlgorithm(), "HS256"
} }
@ -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 @Test
void testParseMalformedJwt() { void testParseMalformedJwt() {

View File

@ -16,11 +16,9 @@
package io.jsonwebtoken.impl package io.jsonwebtoken.impl
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.Jwts import io.jsonwebtoken.*
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.impl.lang.JwtDateConverter
import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.impl.security.TestKeys
import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.DeserializationException
@ -28,6 +26,7 @@ import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Collections import io.jsonwebtoken.lang.Collections
import io.jsonwebtoken.lang.DateFormats
import io.jsonwebtoken.lang.Strings import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
import org.junit.Before import org.junit.Before
@ -251,4 +250,42 @@ class DefaultJwtParserTest {
assertEquals 'whatever', parsedCrit.iterator().next() assertEquals 'whatever', parsedCrit.iterator().next()
assertEquals 42, jwt.getHeader().get('whatever') 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
}
}
} }