Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/main/java/io/jsonwebtoken/SigningKeyResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.jsonwebtoken;

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
Expand Down Expand Up @@ -60,6 +61,17 @@ public interface SigningKeyResolver {
*/
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
* specified header and plaintext payload.
Expand All @@ -70,4 +82,16 @@ public interface SigningKeyResolver {
* specified header and plaintext payload.
*/
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);

}
71 changes: 71 additions & 0 deletions src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import javax.crypto.spec.SecretKeySpec;
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
Expand Down Expand Up @@ -51,6 +53,23 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) {
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<Key>();
for (byte[] keyBytes: keysBytes)
keys.add(new SecretKeySpec(keyBytes, alg.getJcaName()));

return keys;
}

@Override
public Key resolveSigningKey(JwsHeader header, String plaintext) {
SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
Expand All @@ -62,6 +81,23 @@ public Key resolveSigningKey(JwsHeader header, String plaintext) {
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<Key>();
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
* key bytes. This implementation simply throws an exception: if the JWS parsed is a Claims JWS, you must
Expand All @@ -81,6 +117,25 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
"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
* key bytes. This implementation simply throws an exception: if the JWS parsed is a plaintext JWS, you must
Expand All @@ -96,4 +151,20 @@ public byte[] resolveSigningKeyBytes(JwsHeader header, String payload) {
"resolveSigningKey(JwsHeader, String) method or, for HMAC algorithms, the " +
"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.");
}
}
62 changes: 41 additions & 21 deletions src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import java.io.IOException;
import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Map;

Expand All @@ -61,9 +63,9 @@ public class DefaultJwtParser implements JwtParser {

private ObjectMapper objectMapper = new ObjectMapper();

private byte[] keyBytes;
private Collection<byte[]> keyBytes;

private Key key;
private Collection<Key> keys;

private SigningKeyResolver signingKeyResolver;

Expand Down Expand Up @@ -141,21 +143,27 @@ public JwtParser setAllowedClockSkewSeconds(long seconds) {
@Override
public JwtParser setSigningKey(byte[] key) {
Assert.notEmpty(key, "signing key cannot be null or empty.");
this.keyBytes = key;
if (this.keyBytes == null)
this.keyBytes = new ArrayList<byte[]>();
this.keyBytes.add(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);
if (this.keyBytes == null)
this.keyBytes = new ArrayList<byte[]>();
this.keyBytes.add(TextCodec.BASE64.decode(base64EncodedKeyBytes));
return this;
}

@Override
public JwtParser setSigningKey(Key key) {
Assert.notNull(key, "signing key cannot be null.");
this.key = key;
if (this.keys == null)
this.keys = new ArrayList<Key>();
this.keys.add(key);
return this;
}

Expand Down Expand Up @@ -297,47 +305,59 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,
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.");
} else if ((key != null || keyBytes != null) && signingKeyResolver != null) {
String object = key != null ? "a key object" : "key bytes";
} else if ((this.keys != null || this.keyBytes != null) && this.signingKeyResolver != null) {
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.");
}

//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(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (Objects.isEmpty(this.keyBytes) && this.signingKeyResolver != null) { //use the signingKeyResolver
keys = new ArrayList<Key>();
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 {
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(),
"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<Key>();
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:
String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload;

JwtSignatureValidator validator;
try {
validator = createSignatureValidator(algorithm, key);
validator = createSignatureValidator(algorithm, keys);
} catch (IllegalArgumentException e) {
String algName = algorithm.getValue();
Key key = keys.iterator().next();
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 " +
Expand Down Expand Up @@ -468,8 +488,8 @@ private void validateExpectedClaims(Header header, Claims claims) {
/*
* @since 0.5 mostly to allow testing overrides
*/
protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) {
return new DefaultJwtSignatureValidator(alg, key);
protected JwtSignatureValidator createSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
return new DefaultJwtSignatureValidator(alg, keys);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@

import java.nio.charset.Charset;
import java.security.Key;
import java.util.Collection;

public class DefaultJwtSignatureValidator implements JwtSignatureValidator {

private static final Charset US_ASCII = Charset.forName("US-ASCII");

private final SignatureValidator signatureValidator;

public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) {
this(DefaultSignatureValidatorFactory.INSTANCE, alg, key);
public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Collection<Key> keys) {
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.");
this.signatureValidator = factory.createSignatureValidator(alg, key);
this.signatureValidator = factory.createSignatureValidator(alg, keys);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,33 @@
import io.jsonwebtoken.lang.Assert;

import java.security.Key;
import java.util.Collection;

public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory {

public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory();

@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(key, "Signing Key cannot be null.");
Assert.notNull(keys, "Signing Key cannot be null.");

switch (alg) {
case HS256:
case HS384:
case HS512:
return new MacValidator(alg, key);
return new MacValidator(alg, keys);
case RS256:
case RS384:
case RS512:
case PS256:
case PS384:
case PS512:
return new RsaSignatureValidator(alg, key);
return new RsaSignatureValidator(alg, keys);
case ES256:
case ES384:
case ES512:
return new EllipticCurveSignatureValidator(alg, key);
return new EllipticCurveSignatureValidator(alg, keys);
default:
throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
}
Expand Down
Loading