/*
* Copyright (C) 2014 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.IncorrectClaimException;
import io.jsonwebtoken.InvalidClaimException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtHandler;
import io.jsonwebtoken.JwtHandlerAdapter;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.MissingClaimException;
import io.jsonwebtoken.PrematureJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;
import io.jsonwebtoken.impl.crypto.JwtSignatureValidator;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@SuppressWarnings("unchecked")
public class DefaultJwtParser implements JwtParser {
//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 int MILLISECONDS_PER_SECOND = 1000;
private ObjectMapper objectMapper = new ObjectMapper();
private byte[] keyBytes;
private Key key;
private SigningKeyResolver signingKeyResolver;
private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver();
Claims expectedClaims = new DefaultClaims();
private Clock clock = DefaultClock.INSTANCE;
private long allowedClockSkewMillis = 0;
@Override
public JwtParser requireIssuedAt(Date issuedAt) {
expectedClaims.setIssuedAt(issuedAt);
return this;
}
@Override
public JwtParser requireIssuer(String issuer) {
expectedClaims.setIssuer(issuer);
return this;
}
@Override
public JwtParser requireAudience(String audience) {
expectedClaims.setAudience(audience);
return this;
}
@Override
public JwtParser requireSubject(String subject) {
expectedClaims.setSubject(subject);
return this;
}
@Override
public JwtParser requireId(String id) {
expectedClaims.setId(id);
return this;
}
@Override
public JwtParser requireExpiration(Date expiration) {
expectedClaims.setExpiration(expiration);
return this;
}
@Override
public JwtParser requireNotBefore(Date notBefore) {
expectedClaims.setNotBefore(notBefore);
return this;
}
@Override
public JwtParser require(String claimName, Object value) {
Assert.hasText(claimName, "claim name cannot be null or empty.");
Assert.notNull(value, "The value cannot be null for claim name: " + claimName);
expectedClaims.put(claimName, value);
return this;
}
@Override
public JwtParser setClock(Clock clock) {
Assert.notNull(clock, "Clock instance cannot be null.");
this.clock = clock;
return this;
}
@Override
public JwtParser setAllowedClockSkewSeconds(long seconds) {
this.allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND);
return this;
}
@Override
public JwtParser setSigningKey(byte[] key) {
Assert.notEmpty(key, "signing key cannot be null or empty.");
this.keyBytes = key;
return this;
}
@Override
public JwtParser setSigningKey(String base64EncodedKeyBytes) {
Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty.");
this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);
return this;
}
@Override
public JwtParser setSigningKey(Key key) {
Assert.notNull(key, "signing key cannot be null.");
this.key = key;
return this;
}
@Override
public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) {
Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null.");
this.signingKeyResolver = signingKeyResolver;
return this;
}
@Override
public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) {
Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null.");
this.compressionCodecResolver = compressionCodecResolver;
return this;
}
@Override
public boolean isSigned(String jwt) {
if (jwt == null) {
return false;
}
int delimiterCount = 0;
for (int i = 0; i < jwt.length(); i++) {
char c = jwt.charAt(i);
if (delimiterCount == 2) {
return !Character.isWhitespace(c) && c != SEPARATOR_CHAR;
}
if (c == SEPARATOR_CHAR) {
delimiterCount++;
}
}
return false;
}
@Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");
String base64UrlEncodedHeader = null;
String base64UrlEncodedPayload = null;
String base64UrlEncodedDigest = null;
int delimiterCount = 0;
StringBuilder sb = new StringBuilder(128);
for (char c : jwt.toCharArray()) {
if (c == SEPARATOR_CHAR) {
CharSequence tokenSeq = Strings.clean(sb);
String token = tokenSeq!=null?tokenSeq.toString():null;
if (delimiterCount == 0) {
base64UrlEncodedHeader = token;
} else if (delimiterCount == 1) {
base64UrlEncodedPayload = token;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
if (delimiterCount != 2) {
String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
throw new MalformedJwtException(msg);
}
if (sb.length() > 0) {
base64UrlEncodedDigest = sb.toString();
}
if (base64UrlEncodedPayload == null) {
throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload.");
}
// =============== Header =================
Header header = null;
CompressionCodec compressionCodec = null;
if (base64UrlEncodedHeader != null) {
String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader);
Map<String, Object> m = readValue(origValue);
if (base64UrlEncodedDigest != null) {
header = new DefaultJwsHeader(m);
} else {
header = new DefaultHeader(m);
}
compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
}
// =============== Body =================
String payload;
if (compressionCodec != null) {
byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload));
payload = new String(decompressed, Strings.UTF_8);
} else {
payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload);
}
Claims claims = null;
if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it:
Map<String, Object> claimsMap = readValue(payload);
claims = new DefaultClaims(claimsMap);
}
// =============== Signature =================
if (base64UrlEncodedDigest != null) { //it is signed - validate the signature
JwsHeader jwsHeader = (JwsHeader) header;
SignatureAlgorithm algorithm = null;
if (header != null) {
String alg = jwsHeader.getAlgorithm();
if (Strings.hasText(alg)) {
algorithm = SignatureAlgorithm.forName(alg);
}
}
if (algorithm == null || algorithm == SignatureAlgorithm.NONE) {
//it is plaintext, but it has a signature. This is invalid:
String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " +
"algorithm.";
throw new MalformedJwtException(msg);
}
if (key != null && keyBytes != null) {
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
} else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
String object = key != null ? "a key object" : "key bytes";
throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
}
//digitally signed, let's assert the signature:
Key key = this.key;
if (key == null) { //fall back to keyBytes
byte[] keyBytes = this.keyBytes;
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) {
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else {
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
}
}
if (!Objects.isEmpty(keyBytes)) {
Assert.isTrue(algorithm.isHmac(),
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
}
}
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
//re-create the jwt part without the signature. This is what needs to be signed for verification:
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
JwtSignatureValidator validator;
try {
validator = createSignatureValidator(algorithm, key);
} catch (IllegalArgumentException e) {
String algName = algorithm.getValue();
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
"algorithm, but the specified signing key of type " + key.getClass().getName() +
" may not be used to validate " + algName + " signatures. Because the specified " +
"signing key reflects a specific and expected algorithm, and the JWT does not reflect " +
"this algorithm, it is likely that the JWT was not expected and therefore should not be " +
"trusted. Another possibility is that the parser was configured with the incorrect " +
"signing key, but this cannot be assumed for security reasons.";
throw new UnsupportedJwtException(msg, e);
}
if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) {
String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " +
"asserted and should not be trusted.";
throw new SignatureException(msg);
}
}
final boolean allowSkew = this.allowedClockSkewMillis > 0;
//since 0.3:
if (claims != null) {
SimpleDateFormat sdf;
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:
Date exp = claims.getExpiration();
if (exp != null) {
long maxTime = nowTime - this.allowedClockSkewMillis;
Date max = allowSkew ? new Date(maxTime) : now;
if (max.after(exp)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String expVal = sdf.format(exp);
String nowVal = sdf.format(now);
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);
}
}
//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:
Date nbf = claims.getNotBefore();
if (nbf != null) {
long minTime = nowTime + this.allowedClockSkewMillis;
Date min = allowSkew ? new Date(minTime) : now;
if (min.before(nbf)) {
sdf = new SimpleDateFormat(ISO_8601_FORMAT);
String nbfVal = sdf.format(nbf);
String nowVal = sdf.format(now);
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);
}
}
validateExpectedClaims(header, claims);
}
Object body = claims != null ? claims : payload;
if (base64UrlEncodedDigest != null) {
return new DefaultJws<Object>((JwsHeader) header, body, base64UrlEncodedDigest);
} else {
return new DefaultJwt<Object>(header, body);
}
}
private void validateExpectedClaims(Header header, Claims claims) {
for (String expectedClaimName : expectedClaims.keySet()) {
Object expectedClaimValue = expectedClaims.get(expectedClaimName);
Object actualClaimValue = claims.get(expectedClaimName);
if (
Claims.ISSUED_AT.equals(expectedClaimName) ||
Claims.EXPIRATION.equals(expectedClaimName) ||
Claims.NOT_BEFORE.equals(expectedClaimName)
) {
expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
actualClaimValue = claims.get(expectedClaimName, Date.class);
} else if (
expectedClaimValue instanceof Date &&
actualClaimValue != null &&
actualClaimValue instanceof Long
) {
actualClaimValue = new Date((Long)actualClaimValue);
}
InvalidClaimException invalidClaimException = null;
if (actualClaimValue == null) {
String msg = String.format(
ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
expectedClaimName, expectedClaimValue
);
invalidClaimException = new MissingClaimException(header, claims, msg);
} else if (!expectedClaimValue.equals(actualClaimValue)) {
String msg = String.format(
ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE,
expectedClaimName, expectedClaimValue, actualClaimValue
);
invalidClaimException = new IncorrectClaimException(header, claims, msg);
}
if (invalidClaimException != null) {
invalidClaimException.setClaimName(expectedClaimName);
invalidClaimException.setClaimValue(expectedClaimValue);
throw invalidClaimException;
}
}
}
/*
* @since 0.5 mostly to allow testing overrides
*/
protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) {
return new DefaultJwtSignatureValidator(alg, key);
}
@Override
public <T> T parse(String compact, JwtHandler<T> handler)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.notNull(handler, "JwtHandler argument cannot be null.");
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
Jwt jwt = parse(compact);
if (jwt instanceof Jws) {
Jws jws = (Jws) jwt;
Object body = jws.getBody();
if (body instanceof Claims) {
return handler.onClaimsJws((Jws<Claims>) jws);
} else {
return handler.onPlaintextJws((Jws<String>) jws);
}
} else {
Object body = jwt.getBody();
if (body instanceof Claims) {
return handler.onClaimsJwt((Jwt<Header, Claims>) jwt);
} else {
return handler.onPlaintextJwt((Jwt<Header, String>) jwt);
}
}
}
@Override
public Jwt<Header, String> parsePlaintextJwt(String plaintextJwt) {
return parse(plaintextJwt, new JwtHandlerAdapter<Jwt<Header, String>>() {
@Override
public Jwt<Header, String> onPlaintextJwt(Jwt<Header, String> jwt) {
return jwt;
}
});
}
@Override
public Jwt<Header, Claims> parseClaimsJwt(String claimsJwt) {
try {
return parse(claimsJwt, new JwtHandlerAdapter<Jwt<Header, Claims>>() {
@Override
public Jwt<Header, Claims> onClaimsJwt(Jwt<Header, Claims> jwt) {
return jwt;
}
});
} catch (IllegalArgumentException iae) {
throw new UnsupportedJwtException("Signed JWSs are not supported.", iae);
}
}
@Override
public Jws<String> parsePlaintextJws(String plaintextJws) {
try {
return parse(plaintextJws, new JwtHandlerAdapter<Jws<String>>() {
@Override
public Jws<String> onPlaintextJws(Jws<String> jws) {
return jws;
}
});
} catch (IllegalArgumentException iae) {
throw new UnsupportedJwtException("Signed JWSs are not supported.", iae);
}
}
@Override
public Jws<Claims> parseClaimsJws(String claimsJws) {
return parse(claimsJws, new JwtHandlerAdapter<Jws<Claims>>() {
@Override
public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
return jws;
}
});
}
@SuppressWarnings("unchecked")
protected Map<String, Object> readValue(String val) {
try {
return objectMapper.readValue(val, Map.class);
} catch (IOException e) {
throw new MalformedJwtException("Unable to read JSON value: " + val, e);
}
}
}