mirror of https://github.com/jwtk/jjwt.git
add support for supplying multiple public keys that will be attempted when validating a token signature
this facilitates the supporting of certificate rotation, and there being multiple valid keys available during a rotation cycle
This commit is contained in:
parent
ac73b1caa9
commit
ba3ae16141
|
@ -16,6 +16,7 @@
|
||||||
package io.jsonwebtoken;
|
package io.jsonwebtoken;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that
|
* A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that
|
||||||
|
@ -60,6 +61,17 @@ public interface SigningKeyResolver {
|
||||||
*/
|
*/
|
||||||
Key resolveSigningKey(JwsHeader header, Claims claims);
|
Key resolveSigningKey(JwsHeader header, Claims claims);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a collection signing key that should be used to attempt to validate a digital signature for the Claims JWS with the specified
|
||||||
|
* header and claims. This allows for a key rotation scenario to support multiple keys during an overlap period.
|
||||||
|
*
|
||||||
|
* @param header the header of the JWS to validate
|
||||||
|
* @param claims the claims (body) of the JWS to validate
|
||||||
|
* @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified
|
||||||
|
* header and claims.
|
||||||
|
*/
|
||||||
|
Collection<Key> resolveSigningKeys(JwsHeader header, Claims claims);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the
|
* Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the
|
||||||
* specified header and plaintext payload.
|
* specified header and plaintext payload.
|
||||||
|
@ -70,4 +82,16 @@ public interface SigningKeyResolver {
|
||||||
* specified header and plaintext payload.
|
* specified header and plaintext payload.
|
||||||
*/
|
*/
|
||||||
Key resolveSigningKey(JwsHeader header, String plaintext);
|
Key resolveSigningKey(JwsHeader header, String plaintext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the signing key that should be used to attempt to validate a digital signature for the Plaintext JWS with the
|
||||||
|
* specified header and plaintext payload. This allows for a key rotation scenario to support multiple keys during an overlap period.
|
||||||
|
*
|
||||||
|
* @param header the header of the JWS to validate
|
||||||
|
* @param plaintext the plaintext body of the JWS to validate
|
||||||
|
* @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the
|
||||||
|
* specified header and plaintext payload.
|
||||||
|
*/
|
||||||
|
Collection<Key> resolveSigningKeys(JwsHeader header, String plaintext);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An <a href="http://en.wikipedia.org/wiki/Adapter_pattern">Adapter</a> implementation of the
|
* An <a href="http://en.wikipedia.org/wiki/Adapter_pattern">Adapter</a> implementation of the
|
||||||
|
@ -51,6 +53,23 @@ public class SigningKeyResolverAdapter implements SigningKeyResolver {
|
||||||
return new SecretKeySpec(keyBytes, alg.getJcaName());
|
return new SecretKeySpec(keyBytes, alg.getJcaName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Key> resolveSigningKeys(JwsHeader header, Claims claims) {
|
||||||
|
SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
|
||||||
|
Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, Claims) implementation cannot be " +
|
||||||
|
"used for asymmetric key algorithms (RSA, Elliptic Curve). " +
|
||||||
|
"Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " +
|
||||||
|
"Key instance appropriate for the " + alg.name() + " algorithm.");
|
||||||
|
Collection<byte[]> keysBytes = resolveSigningKeysBytes(header, claims);
|
||||||
|
if (keysBytes == null)
|
||||||
|
return null;
|
||||||
|
Collection<Key> keys = new ArrayList<>();
|
||||||
|
for (byte[] keyBytes: keysBytes)
|
||||||
|
keys.add(new SecretKeySpec(keyBytes, alg.getJcaName()));
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Key resolveSigningKey(JwsHeader header, String plaintext) {
|
public Key resolveSigningKey(JwsHeader header, String plaintext) {
|
||||||
SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
|
SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
|
||||||
|
@ -62,6 +81,23 @@ public class SigningKeyResolverAdapter implements SigningKeyResolver {
|
||||||
return new SecretKeySpec(keyBytes, alg.getJcaName());
|
return new SecretKeySpec(keyBytes, alg.getJcaName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Key> resolveSigningKeys(JwsHeader header, String plaintext) {
|
||||||
|
SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
|
||||||
|
Assert.isTrue(alg.isHmac(), "The default resolveSigningKey(JwsHeader, String) implementation cannot be " +
|
||||||
|
"used for asymmetric key algorithms (RSA, Elliptic Curve). " +
|
||||||
|
"Override the resolveSigningKey(JwsHeader, String) method instead and return a " +
|
||||||
|
"Key instance appropriate for the " + alg.name() + " algorithm.");
|
||||||
|
Collection<byte[]> keysBytes = resolveSigningKeysBytes(header, plaintext);
|
||||||
|
if (keysBytes == null)
|
||||||
|
return null;
|
||||||
|
Collection<Key> keys = new ArrayList<>();
|
||||||
|
for (byte[] keyBytes: keysBytes)
|
||||||
|
keys.add(new SecretKeySpec(keyBytes, alg.getJcaName()));
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing
|
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing
|
||||||
* key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must
|
* key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must
|
||||||
|
@ -81,6 +117,25 @@ public class SigningKeyResolverAdapter implements SigningKeyResolver {
|
||||||
"resolveSigningKeyBytes(JwsHeader, Claims) method.");
|
"resolveSigningKeyBytes(JwsHeader, Claims) method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, Claims)} that obtains the necessary signing
|
||||||
|
* key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must
|
||||||
|
* override this method or the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.
|
||||||
|
*
|
||||||
|
* <p><b>NOTE:</b> You cannot override this method when validating RSA signatures. If you expect RSA signatures,
|
||||||
|
* you must override the {@link #resolveSigningKey(JwsHeader, Claims)} method instead.</p>
|
||||||
|
*
|
||||||
|
* @param header the parsed {@link JwsHeader}
|
||||||
|
* @param claims the parsed {@link Claims}
|
||||||
|
* @return the signing key bytes to use to verify the JWS signature.
|
||||||
|
*/
|
||||||
|
public Collection<byte[]> resolveSigningKeysBytes(JwsHeader header, Claims claims) {
|
||||||
|
throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " +
|
||||||
|
"Claims JWS signing key resolution. Consider overriding either the " +
|
||||||
|
"resolveSigningKey(JwsHeader, Claims) method or, for HMAC algorithms, the " +
|
||||||
|
"resolveSigningKeyBytes(JwsHeader, Claims) method.");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing
|
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing
|
||||||
* key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must
|
* key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must
|
||||||
|
@ -96,4 +151,20 @@ public class SigningKeyResolverAdapter implements SigningKeyResolver {
|
||||||
"resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " +
|
"resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " +
|
||||||
"resolveSigningKeyBytes(JwsHeader, String) method.");
|
"resolveSigningKeyBytes(JwsHeader, String) method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience method invoked by {@link #resolveSigningKey(JwsHeader, String)} that obtains the necessary signing
|
||||||
|
* key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must
|
||||||
|
* override this method or the {@link #resolveSigningKey(JwsHeader, String)} method instead.
|
||||||
|
*
|
||||||
|
* @param header the parsed {@link JwsHeader}
|
||||||
|
* @param payload the parsed String plaintext payload
|
||||||
|
* @return the signing key bytes to use to verify the JWS signature.
|
||||||
|
*/
|
||||||
|
public Collection<byte[]> resolveSigningKeysBytes(JwsHeader header, String payload) {
|
||||||
|
throw new UnsupportedJwtException("The specified SigningKeyResolver implementation does not support " +
|
||||||
|
"plaintext JWS signing key resolution. Consider overriding either the " +
|
||||||
|
"resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " +
|
||||||
|
"resolveSigningKeyBytes(JwsHeader, String) method.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,8 @@ import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -59,11 +61,11 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
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 static final int MILLISECONDS_PER_SECOND = 1000;
|
||||||
|
|
||||||
private ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
private byte[] keyBytes;
|
private Collection<byte[]> keyBytes;
|
||||||
|
|
||||||
private Key key;
|
private Collection<Key> keys;
|
||||||
|
|
||||||
private SigningKeyResolver signingKeyResolver;
|
private SigningKeyResolver signingKeyResolver;
|
||||||
|
|
||||||
|
@ -77,43 +79,43 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireIssuedAt(Date issuedAt) {
|
public JwtParser requireIssuedAt(Date issuedAt) {
|
||||||
expectedClaims.setIssuedAt(issuedAt);
|
this.expectedClaims.setIssuedAt(issuedAt);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireIssuer(String issuer) {
|
public JwtParser requireIssuer(String issuer) {
|
||||||
expectedClaims.setIssuer(issuer);
|
this.expectedClaims.setIssuer(issuer);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireAudience(String audience) {
|
public JwtParser requireAudience(String audience) {
|
||||||
expectedClaims.setAudience(audience);
|
this.expectedClaims.setAudience(audience);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireSubject(String subject) {
|
public JwtParser requireSubject(String subject) {
|
||||||
expectedClaims.setSubject(subject);
|
this.expectedClaims.setSubject(subject);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireId(String id) {
|
public JwtParser requireId(String id) {
|
||||||
expectedClaims.setId(id);
|
this.expectedClaims.setId(id);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireExpiration(Date expiration) {
|
public JwtParser requireExpiration(Date expiration) {
|
||||||
expectedClaims.setExpiration(expiration);
|
this.expectedClaims.setExpiration(expiration);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser requireNotBefore(Date notBefore) {
|
public JwtParser requireNotBefore(Date notBefore) {
|
||||||
expectedClaims.setNotBefore(notBefore);
|
this.expectedClaims.setNotBefore(notBefore);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +123,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
public JwtParser require(String claimName, Object value) {
|
public JwtParser require(String claimName, Object value) {
|
||||||
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);
|
this.expectedClaims.put(claimName, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,21 +143,27 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
@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.");
|
||||||
this.keyBytes = key;
|
if (this.keyBytes == null)
|
||||||
|
this.keyBytes = new ArrayList<>();
|
||||||
|
this.keyBytes.add(key);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser setSigningKey(String base64EncodedKeyBytes) {
|
public JwtParser setSigningKey(String base64EncodedKeyBytes) {
|
||||||
Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty.");
|
Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty.");
|
||||||
this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes);
|
if (this.keyBytes == null)
|
||||||
|
this.keyBytes = new ArrayList<>();
|
||||||
|
this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JwtParser setSigningKey(Key key) {
|
public JwtParser setSigningKey(Key key) {
|
||||||
Assert.notNull(key, "signing key cannot be null.");
|
Assert.notNull(key, "signing key cannot be null.");
|
||||||
this.key = key;
|
if (this.keys == null)
|
||||||
|
this.keys = new ArrayList<>();
|
||||||
|
this.keys.add(key);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +265,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
header = new DefaultHeader(m);
|
header = new DefaultHeader(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
compressionCodec = compressionCodecResolver.resolveCompressionCodec(header);
|
compressionCodec = this.compressionCodecResolver.resolveCompressionCodec(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============== Body =================
|
// =============== Body =================
|
||||||
|
@ -297,47 +305,59 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
throw new MalformedJwtException(msg);
|
throw new MalformedJwtException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key != null && keyBytes != null) {
|
if (this.keys != null && this.keyBytes != null) {
|
||||||
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
|
throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either.");
|
||||||
} else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
|
} else if ((this.keys != null || this.keyBytes != null) && this.signingKeyResolver != null) {
|
||||||
String object = key != null ? "a key object" : "key bytes";
|
String object = this.keys != null ? "a key object" : "key bytes";
|
||||||
throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
|
throw new IllegalStateException("A signing key resolver and " + object + " cannot both be specified. Choose either.");
|
||||||
}
|
}
|
||||||
|
|
||||||
//digitally signed, let's assert the signature:
|
//digitally signed, let's assert the signature:
|
||||||
Key key = this.key;
|
Collection<Key> keys = this.keys;
|
||||||
|
|
||||||
if (key == null) { //fall back to keyBytes
|
if (keys == null) { //fall back to keyBytes
|
||||||
|
|
||||||
byte[] keyBytes = this.keyBytes;
|
if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver
|
||||||
|
keys = new ArrayList<>();
|
||||||
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
|
|
||||||
if (claims != null) {
|
if (claims != null) {
|
||||||
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
|
Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, claims);
|
||||||
|
if (key != null)
|
||||||
|
keys.add(key);
|
||||||
|
Collection<Key> keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, claims);
|
||||||
|
if (!Objects.isEmpty(keyList))
|
||||||
|
keys.addAll(keyList);
|
||||||
} else {
|
} else {
|
||||||
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
|
Key key = this.signingKeyResolver.resolveSigningKey(jwsHeader, payload);
|
||||||
|
if (key != null)
|
||||||
|
keys.add(key);
|
||||||
|
Collection<Key> keyList = this.signingKeyResolver.resolveSigningKeys(jwsHeader, payload);
|
||||||
|
if (!Objects.isEmpty(keyList))
|
||||||
|
keys.addAll(keyList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Objects.isEmpty(keyBytes)) {
|
if (!Objects.isEmpty(this.keyBytes)) {
|
||||||
|
|
||||||
Assert.isTrue(algorithm.isHmac(),
|
Assert.isTrue(algorithm.isHmac(),
|
||||||
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
|
"Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.");
|
||||||
|
|
||||||
key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
|
keys = new ArrayList<>();
|
||||||
|
for (byte[] bytes: this.keyBytes)
|
||||||
|
this.keys.add(new SecretKeySpec(bytes, algorithm.getJcaName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed.");
|
Assert.notNull(keys, "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:
|
//re-create the jwt part without the signature. This is what needs to be signed for verification:
|
||||||
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
|
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;
|
||||||
|
|
||||||
JwtSignatureValidator validator;
|
JwtSignatureValidator validator;
|
||||||
try {
|
try {
|
||||||
validator = createSignatureValidator(algorithm, key);
|
validator = createSignatureValidator(algorithm, keys);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
String algName = algorithm.getValue();
|
String algName = algorithm.getValue();
|
||||||
|
Key key = keys.iterator().next();
|
||||||
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
|
String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " +
|
||||||
"algorithm, but the specified signing key of type " + key.getClass().getName() +
|
"algorithm, but the specified signing key of type " + key.getClass().getName() +
|
||||||
" may not be used to validate " + algName + " signatures. Because the specified " +
|
" may not be used to validate " + algName + " signatures. Because the specified " +
|
||||||
|
@ -421,9 +441,9 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateExpectedClaims(Header header, Claims claims) {
|
private void validateExpectedClaims(Header header, Claims claims) {
|
||||||
for (String expectedClaimName : expectedClaims.keySet()) {
|
for (String expectedClaimName : this.expectedClaims.keySet()) {
|
||||||
|
|
||||||
Object expectedClaimValue = expectedClaims.get(expectedClaimName);
|
Object expectedClaimValue = this.expectedClaims.get(expectedClaimName);
|
||||||
Object actualClaimValue = claims.get(expectedClaimName);
|
Object actualClaimValue = claims.get(expectedClaimName);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -431,7 +451,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
Claims.EXPIRATION.equals(expectedClaimName) ||
|
Claims.EXPIRATION.equals(expectedClaimName) ||
|
||||||
Claims.NOT_BEFORE.equals(expectedClaimName)
|
Claims.NOT_BEFORE.equals(expectedClaimName)
|
||||||
) {
|
) {
|
||||||
expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class);
|
expectedClaimValue = this.expectedClaims.get(expectedClaimName, Date.class);
|
||||||
actualClaimValue = claims.get(expectedClaimName, Date.class);
|
actualClaimValue = claims.get(expectedClaimName, Date.class);
|
||||||
} else if (
|
} else if (
|
||||||
expectedClaimValue instanceof Date &&
|
expectedClaimValue instanceof Date &&
|
||||||
|
@ -468,8 +488,8 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
/*
|
/*
|
||||||
* @since 0.5 mostly to allow testing overrides
|
* @since 0.5 mostly to allow testing overrides
|
||||||
*/
|
*/
|
||||||
protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) {
|
protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
return new DefaultJwtSignatureValidator(alg, key);
|
return new DefaultJwtSignatureValidator(alg, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -484,16 +504,16 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
Jws jws = (Jws) jwt;
|
Jws jws = (Jws) jwt;
|
||||||
Object body = jws.getBody();
|
Object body = jws.getBody();
|
||||||
if (body instanceof Claims) {
|
if (body instanceof Claims) {
|
||||||
return handler.onClaimsJws((Jws<Claims>) jws);
|
return handler.onClaimsJws(jws);
|
||||||
} else {
|
} else {
|
||||||
return handler.onPlaintextJws((Jws<String>) jws);
|
return handler.onPlaintextJws(jws);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Object body = jwt.getBody();
|
Object body = jwt.getBody();
|
||||||
if (body instanceof Claims) {
|
if (body instanceof Claims) {
|
||||||
return handler.onClaimsJwt((Jwt<Header, Claims>) jwt);
|
return handler.onClaimsJwt(jwt);
|
||||||
} else {
|
} else {
|
||||||
return handler.onPlaintextJwt((Jwt<Header, String>) jwt);
|
return handler.onPlaintextJwt(jwt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -549,7 +569,7 @@ public class DefaultJwtParser implements JwtParser {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
protected Map<String, Object> readValue(String val) {
|
protected Map<String, Object> readValue(String val) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(val, Map.class);
|
return this.objectMapper.readValue(val, Map.class);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new MalformedJwtException("Unable to read JSON value: " + val, e);
|
throw new MalformedJwtException("Unable to read JSON value: " + val, e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public class DefaultJwtSignatureValidator implements JwtSignatureValidator {
|
public class DefaultJwtSignatureValidator implements JwtSignatureValidator {
|
||||||
|
|
||||||
|
@ -28,13 +29,13 @@ public class DefaultJwtSignatureValidator implements JwtSignatureValidator {
|
||||||
|
|
||||||
private final SignatureValidator signatureValidator;
|
private final SignatureValidator signatureValidator;
|
||||||
|
|
||||||
public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) {
|
public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
this(DefaultSignatureValidatorFactory.INSTANCE, alg, key);
|
this(DefaultSignatureValidatorFactory.INSTANCE, alg, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) {
|
public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
Assert.notNull(factory, "SignerFactory argument cannot be null.");
|
Assert.notNull(factory, "SignerFactory argument cannot be null.");
|
||||||
this.signatureValidator = factory.createSignatureValidator(alg, key);
|
this.signatureValidator = factory.createSignatureValidator(alg, keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,32 +19,33 @@ import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory {
|
public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory {
|
||||||
|
|
||||||
public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory();
|
public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) {
|
public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
|
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
|
||||||
Assert.notNull(key, "Signing Key cannot be null.");
|
Assert.notNull(keys, "Signing Key cannot be null.");
|
||||||
|
|
||||||
switch (alg) {
|
switch (alg) {
|
||||||
case HS256:
|
case HS256:
|
||||||
case HS384:
|
case HS384:
|
||||||
case HS512:
|
case HS512:
|
||||||
return new MacValidator(alg, key);
|
return new MacValidator(alg, keys);
|
||||||
case RS256:
|
case RS256:
|
||||||
case RS384:
|
case RS384:
|
||||||
case RS512:
|
case RS512:
|
||||||
case PS256:
|
case PS256:
|
||||||
case PS384:
|
case PS384:
|
||||||
case PS512:
|
case PS512:
|
||||||
return new RsaSignatureValidator(alg, key);
|
return new RsaSignatureValidator(alg, keys);
|
||||||
case ES256:
|
case ES256:
|
||||||
case ES384:
|
case ES384:
|
||||||
case ES512:
|
case ES512:
|
||||||
return new EllipticCurveSignatureValidator(alg, key);
|
return new EllipticCurveSignatureValidator(alg, keys);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
|
throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.security.Key;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
import java.security.interfaces.ECPublicKey;
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.SignatureException;
|
import io.jsonwebtoken.SignatureException;
|
||||||
|
@ -30,17 +31,22 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple
|
||||||
private static final String EC_PUBLIC_KEY_REQD_MSG =
|
private static final String EC_PUBLIC_KEY_REQD_MSG =
|
||||||
"Elliptic Curve signature validation requires an ECPublicKey instance.";
|
"Elliptic Curve signature validation requires an ECPublicKey instance.";
|
||||||
|
|
||||||
public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) {
|
private final Collection<Key> keys;
|
||||||
super(alg, key);
|
|
||||||
|
public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
|
super(alg, null);
|
||||||
|
this.keys = keys;
|
||||||
|
for (Key key: keys)
|
||||||
Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG);
|
Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(byte[] data, byte[] signature) {
|
public boolean isValid(byte[] data, byte[] signature) {
|
||||||
Signature sig = createSignatureInstance();
|
Signature sig = createSignatureInstance();
|
||||||
|
for (Key key: this.keys) {
|
||||||
PublicKey publicKey = (PublicKey) key;
|
PublicKey publicKey = (PublicKey) key;
|
||||||
try {
|
try {
|
||||||
int expectedSize = getSignatureByteArrayLength(alg);
|
int expectedSize = getSignatureByteArrayLength(this.alg);
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* If the expected size is not valid for JOSE, fall back to ASN.1 DER signature.
|
* If the expected size is not valid for JOSE, fall back to ASN.1 DER signature.
|
||||||
|
@ -49,12 +55,15 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple
|
||||||
*
|
*
|
||||||
* **/
|
* **/
|
||||||
byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature);
|
byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature);
|
||||||
return doVerify(sig, publicKey, data, derSignature);
|
if (doVerify(sig, publicKey, data, derSignature))
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage();
|
String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage();
|
||||||
throw new SignatureException(msg, e);
|
throw new SignatureException(msg, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature)
|
protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature)
|
||||||
throws InvalidKeyException, java.security.SignatureException {
|
throws InvalidKeyException, java.security.SignatureException {
|
||||||
|
|
|
@ -19,19 +19,27 @@ import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Arrays;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public class MacValidator implements SignatureValidator {
|
public class MacValidator implements SignatureValidator {
|
||||||
|
|
||||||
private final MacSigner signer;
|
private final Collection<MacSigner> signers;
|
||||||
|
|
||||||
public MacValidator(SignatureAlgorithm alg, Key key) {
|
public MacValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
this.signer = new MacSigner(alg, key);
|
Collection<MacSigner> signers = new ArrayList<>();
|
||||||
|
for (Key key: keys)
|
||||||
|
signers.add(new MacSigner(alg, key));
|
||||||
|
this.signers = signers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(byte[] data, byte[] signature) {
|
public boolean isValid(byte[] data, byte[] signature) {
|
||||||
byte[] computed = this.signer.sign(data);
|
for (MacSigner signer: this.signers) {
|
||||||
return MessageDigest.isEqual(computed, signature);
|
byte[] computed = signer.sign(data);
|
||||||
|
if (MessageDigest.isEqual(computed, signature))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,36 +25,59 @@ import java.security.PublicKey;
|
||||||
import java.security.Signature;
|
import java.security.Signature;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public class RsaSignatureValidator extends RsaProvider implements SignatureValidator {
|
public class RsaSignatureValidator extends RsaProvider implements SignatureValidator {
|
||||||
|
|
||||||
private final RsaSigner SIGNER;
|
private final Collection<SignerAndKey> SIGNERS;
|
||||||
|
|
||||||
public RsaSignatureValidator(SignatureAlgorithm alg, Key key) {
|
public static final class SignerAndKey {
|
||||||
super(alg, key);
|
|
||||||
|
private final RsaSigner signer;
|
||||||
|
private final Key key;
|
||||||
|
|
||||||
|
public SignerAndKey(final RsaSigner signer, final Key key) {
|
||||||
|
this.signer = signer;
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RsaSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
|
||||||
|
super(alg, null);
|
||||||
|
|
||||||
|
Collection<SignerAndKey> SIGNERS = new ArrayList<>();
|
||||||
|
for (Key key: keys) {
|
||||||
Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey,
|
Assert.isTrue(key instanceof RSAPrivateKey || key instanceof RSAPublicKey,
|
||||||
"RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance.");
|
"RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance.");
|
||||||
this.SIGNER = key instanceof RSAPrivateKey ? new RsaSigner(alg, key) : null;
|
SIGNERS.add(new SignerAndKey(new RsaSigner(alg, key), key));
|
||||||
|
}
|
||||||
|
this.SIGNERS = SIGNERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid(byte[] data, byte[] signature) {
|
public boolean isValid(byte[] data, byte[] signature) {
|
||||||
if (key instanceof PublicKey) {
|
for (SignerAndKey signerAndKey: this.SIGNERS) {
|
||||||
|
if (signerAndKey.key instanceof PublicKey) {
|
||||||
Signature sig = createSignatureInstance();
|
Signature sig = createSignatureInstance();
|
||||||
PublicKey publicKey = (PublicKey) key;
|
PublicKey publicKey = (PublicKey) signerAndKey.key;
|
||||||
try {
|
try {
|
||||||
return doVerify(sig, publicKey, data, signature);
|
if (doVerify(sig, publicKey, data, signature))
|
||||||
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage();
|
String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage();
|
||||||
throw new SignatureException(msg, e);
|
throw new SignatureException(msg, e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Assert.notNull(this.SIGNER, "RSA Signer instance cannot be null. This is a bug. Please report it.");
|
Assert.notNull(this.SIGNERS, "RSA Signer instance cannot be null. This is a bug. Please report it.");
|
||||||
byte[] computed = this.SIGNER.sign(data);
|
byte[] computed = signerAndKey.signer.sign(data);
|
||||||
return Arrays.equals(computed, signature);
|
if (Arrays.equals(computed, signature))
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature)
|
protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature)
|
||||||
throws InvalidKeyException, java.security.SignatureException {
|
throws InvalidKeyException, java.security.SignatureException {
|
||||||
|
|
|
@ -18,8 +18,9 @@ package io.jsonwebtoken.impl.crypto;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public interface SignatureValidatorFactory {
|
public interface SignatureValidatorFactory {
|
||||||
|
|
||||||
SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key);
|
SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Array;
|
import java.lang.reflect.Array;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public final class Objects {
|
public final class Objects {
|
||||||
|
|
||||||
|
@ -95,6 +96,16 @@ public final class Objects {
|
||||||
return (array == null || array.length == 0);
|
return (array == null || array.length == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the given collection is empty:
|
||||||
|
* i.e. <code>null</code> or of zero length.
|
||||||
|
*
|
||||||
|
* @param array the Collection to check
|
||||||
|
*/
|
||||||
|
public static boolean isEmpty(Collection<?> collection) {
|
||||||
|
return (collection == null || collection.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise.
|
* Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue