mirror of https://github.com/jwtk/jjwt.git
Merge branch '61-edits' and MichaelSims-master
This commit is contained in:
commit
b13650dc60
|
@ -131,11 +131,21 @@ public interface JwtParser {
|
||||||
* The parser uses a {@link DefaultClock DefaultClock} instance by default.
|
* 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.
|
* @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT.
|
||||||
* @return the builder instance for method chaining.
|
* @return the parser for method chaining.
|
||||||
* @since 0.7.0
|
* @since 0.7.0
|
||||||
*/
|
*/
|
||||||
JwtParser setClock(Clock clock);
|
JwtParser setClock(Clock clock);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the amount of clock skew in seconds to tolerate when verifying the local time against the {@code exp}
|
||||||
|
* and {@code nbf} claims.
|
||||||
|
*
|
||||||
|
* @param seconds the number of seconds to tolerate for clock skew when verifying {@code exp} or {@code nbf} claims.
|
||||||
|
* @return the parser for method chaining.
|
||||||
|
* @since 0.7.0
|
||||||
|
*/
|
||||||
|
JwtParser setAllowedClockSkewSeconds(long seconds);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not
|
* 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.
|
* a JWS (no signature), this key is not used.
|
||||||
|
|
|
@ -57,6 +57,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
|
|
||||||
//don't need millis since JWT date fields are only second granularity:
|
//don't need millis since JWT date fields are only second granularity:
|
||||||
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||||
|
private static final int MILLISECONDS_PER_SECOND = 1000;
|
||||||
|
|
||||||
private ObjectMapper objectMapper = new ObjectMapper();
|
private ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@ -72,52 +73,47 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
|
|
||||||
private Clock clock = DefaultClock.INSTANCE;
|
private Clock clock = DefaultClock.INSTANCE;
|
||||||
|
|
||||||
|
private long allowedClockSkewMillis = 0;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireIssuedAt(Date issuedAt) {
|
public JwtParser requireIssuedAt(Date issuedAt) {
|
||||||
expectedClaims.setIssuedAt(issuedAt);
|
expectedClaims.setIssuedAt(issuedAt);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireIssuer(String issuer) {
|
public JwtParser requireIssuer(String issuer) {
|
||||||
expectedClaims.setIssuer(issuer);
|
expectedClaims.setIssuer(issuer);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireAudience(String audience) {
|
public JwtParser requireAudience(String audience) {
|
||||||
expectedClaims.setAudience(audience);
|
expectedClaims.setAudience(audience);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireSubject(String subject) {
|
public JwtParser requireSubject(String subject) {
|
||||||
expectedClaims.setSubject(subject);
|
expectedClaims.setSubject(subject);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireId(String id) {
|
public JwtParser requireId(String id) {
|
||||||
expectedClaims.setId(id);
|
expectedClaims.setId(id);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireExpiration(Date expiration) {
|
public JwtParser requireExpiration(Date expiration) {
|
||||||
expectedClaims.setExpiration(expiration);
|
expectedClaims.setExpiration(expiration);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireNotBefore(Date notBefore) {
|
public JwtParser requireNotBefore(Date notBefore) {
|
||||||
expectedClaims.setNotBefore(notBefore);
|
expectedClaims.setNotBefore(notBefore);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +122,6 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
Assert.hasText(claimName, "claim name cannot be null or empty.");
|
Assert.hasText(claimName, "claim name cannot be null or empty.");
|
||||||
Assert.notNull(value, "The value cannot be null for claim name: " + claimName);
|
Assert.notNull(value, "The value cannot be null for claim name: " + claimName);
|
||||||
expectedClaims.put(claimName, value);
|
expectedClaims.put(claimName, value);
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +132,12 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JwtParser setAllowedClockSkewSeconds(long seconds) {
|
||||||
|
this.allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser setSigningKey(byte[] key) {
|
public JwtParser setSigningKey(byte[] key) {
|
||||||
Assert.notEmpty(key, "signing key cannot be null or empty.");
|
Assert.notEmpty(key, "signing key cannot be null or empty.");
|
||||||
|
@ -354,24 +355,33 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final boolean allowSkew = this.allowedClockSkewMillis > 0;
|
||||||
|
|
||||||
//since 0.3:
|
//since 0.3:
|
||||||
if (claims != null) {
|
if (claims != null) {
|
||||||
|
|
||||||
SimpleDateFormat sdf;
|
SimpleDateFormat sdf;
|
||||||
|
|
||||||
final Date now = this.clock.now();
|
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
|
//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:
|
//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) {
|
||||||
|
|
||||||
if (now.equals(exp) || now.after(exp)) {
|
long maxTime = nowTime - this.allowedClockSkewMillis;
|
||||||
|
Date max = allowSkew ? new Date(maxTime) : now;
|
||||||
|
if (max.after(exp)) {
|
||||||
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
||||||
String expVal = sdf.format(exp);
|
String expVal = sdf.format(exp);
|
||||||
String nowVal = sdf.format(now);
|
String nowVal = sdf.format(now);
|
||||||
|
|
||||||
String msg = "JWT expired at " + expVal + ". Current time: " + nowVal;
|
long differenceMillis = maxTime - exp.getTime();
|
||||||
|
|
||||||
|
String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " +
|
||||||
|
differenceMillis + " milliseconds. Allowed clock skew: " +
|
||||||
|
this.allowedClockSkewMillis + " milliseconds.";
|
||||||
throw new ExpiredJwtException(header, claims, msg);
|
throw new ExpiredJwtException(header, claims, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,12 +391,19 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
Date nbf = claims.getNotBefore();
|
Date nbf = claims.getNotBefore();
|
||||||
if (nbf != null) {
|
if (nbf != null) {
|
||||||
|
|
||||||
if (now.before(nbf)) {
|
long minTime = nowTime + this.allowedClockSkewMillis;
|
||||||
|
Date min = allowSkew ? new Date(minTime) : now;
|
||||||
|
if (min.before(nbf)) {
|
||||||
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
|
||||||
String nbfVal = sdf.format(nbf);
|
String nbfVal = sdf.format(nbf);
|
||||||
String nowVal = sdf.format(now);
|
String nowVal = sdf.format(now);
|
||||||
|
|
||||||
String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal;
|
long differenceMillis = nbf.getTime() - minTime;
|
||||||
|
|
||||||
|
String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal +
|
||||||
|
", a difference of " +
|
||||||
|
differenceMillis + " milliseconds. Allowed clock skew: " +
|
||||||
|
this.allowedClockSkewMillis + " milliseconds.";
|
||||||
throw new PrematureJwtException(header, claims, msg);
|
throw new PrematureJwtException(header, claims, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,8 +176,8 @@ class JwtParserTest {
|
||||||
} catch (ExpiredJwtException e) {
|
} catch (ExpiredJwtException e) {
|
||||||
assertTrue e.getMessage().startsWith('JWT expired at ')
|
assertTrue e.getMessage().startsWith('JWT expired at ')
|
||||||
|
|
||||||
//https://github.com/jwtk/jjwt/issues/107 :
|
//https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp):
|
||||||
assertTrue e.getMessage().endsWith('Z')
|
assertTrue e.getMessage().contains('Z, a difference of ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,8 +194,60 @@ class JwtParserTest {
|
||||||
} catch (PrematureJwtException e) {
|
} catch (PrematureJwtException e) {
|
||||||
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
|
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
|
||||||
|
|
||||||
//https://github.com/jwtk/jjwt/issues/107 :
|
//https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp):
|
||||||
assertTrue e.getMessage().endsWith('Z')
|
assertTrue e.getMessage().contains('Z, a difference of ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseWithExpiredJwtWithinAllowedClockSkew() {
|
||||||
|
Date exp = new Date(System.currentTimeMillis() - 3000)
|
||||||
|
|
||||||
|
String subject = 'Joe'
|
||||||
|
String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact()
|
||||||
|
|
||||||
|
Jwt<Header,Claims> jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact)
|
||||||
|
|
||||||
|
assertEquals jwt.getBody().getSubject(), subject
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseWithExpiredJwtNotWithinAllowedClockSkew() {
|
||||||
|
Date exp = new Date(System.currentTimeMillis() - 3000)
|
||||||
|
|
||||||
|
String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact()
|
||||||
|
|
||||||
|
try {
|
||||||
|
Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact)
|
||||||
|
fail()
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
assertTrue e.getMessage().startsWith('JWT expired at ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseWithPrematureJwtWithinAllowedClockSkew() {
|
||||||
|
Date exp = new Date(System.currentTimeMillis() + 3000)
|
||||||
|
|
||||||
|
String subject = 'Joe'
|
||||||
|
String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact()
|
||||||
|
|
||||||
|
Jwt<Header,Claims> jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact)
|
||||||
|
|
||||||
|
assertEquals jwt.getBody().getSubject(), subject
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseWithPrematureJwtNotWithinAllowedClockSkew() {
|
||||||
|
Date exp = new Date(System.currentTimeMillis() + 3000)
|
||||||
|
|
||||||
|
String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact()
|
||||||
|
|
||||||
|
try {
|
||||||
|
Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact)
|
||||||
|
fail()
|
||||||
|
} catch (PrematureJwtException e) {
|
||||||
|
assertTrue e.getMessage().startsWith('JWT must not be accepted before ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue