updated SigningKeyResolver concept to handle standard java.security.Key instances, and updated the SigningKeyResolverAdapter to include bytes convenience methods.

This commit is contained in:
Les Hazlewood 2014-11-19 19:00:54 -08:00
parent f59684488f
commit ec643b4556
5 changed files with 151 additions and 62 deletions

View File

@ -78,17 +78,32 @@ public interface JwtParser {
JwtParser setSigningKey(Key key); JwtParser setSigningKey(Key key);
/** /**
* Sets the {@link SigningKeyResolver} used to resolve the <code>signing key</code> using the parsed {@link JwsHeader} * Sets the {@link SigningKeyResolver} used to acquire the <code>signing key</code> that should be used to verify
* and/or the {@link Claims}. If the specified JWT string is not a JWS (no signature), this resolver is not used. * a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used.
* <p/> *
* <p>This method will set the signing key resolver to be used in case a signing key is not provided by any of the other methods.</p> * <p>Specifying a {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing
* <p/> * the JWT and the JWT header or payload (plaintext body or Claims) must be inspected first to determine how to
* <p>This is a convenience method: the {@code jwsSignatureKeyResolver} is used after a Jws has been parsed and either the * look up the signing key. Once returned by the resolver, the JwtParser will then verify the JWS signature with the
* {@link JwsHeader} or the {@link Claims} embedded in the {@link Jws} can be used to resolve the signing key. * returned key. For example:</p>
* </p> *
* <pre>
* Jws&lt;Claims&gt; jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
* &#64;Override
* public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
* //inspect the header or claims, lookup and return the signing key
* return getSigningKey(header, claims); //implement me
* }})
* .parseClaimsJws(compact);
* </pre>
*
* <p>A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.</p>
*
* <p>This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder
* methods.</p>
* *
* @param signingKeyResolver the signing key resolver used to retrieve the signing key. * @param signingKeyResolver the signing key resolver used to retrieve the signing key.
* @return the parser for method chaining. * @return the parser for method chaining.
* @since 0.4
*/ */
JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver);
@ -96,7 +111,7 @@ public interface JwtParser {
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise. * otherwise.
* *
* <p>Note that if you are reasonably sure that the token is signed, it is usually more efficient to attempt to * <p>Note that if you are reasonably sure that the token is signed, it is more efficient to attempt to
* parse the token (and catching exceptions if necessary) instead of calling this method first before parsing.</p> * parse the token (and catching exceptions if necessary) instead of calling this method first before parsing.</p>
* *
* @param jwt the compact serialized JWT to check * @param jwt the compact serialized JWT to check

View File

@ -15,44 +15,59 @@
*/ */
package io.jsonwebtoken; package io.jsonwebtoken;
import java.security.Key;
/** /**
* A JwsSigningKeyResolver is invoked by a {@link io.jsonwebtoken.JwtParser JwtParser} if it's provided and the * A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that
* JWT being parsed is signed. * should be used to verify a JWS signature.
* <p/>
* Implementations of this interfaces must be provided to {@link io.jsonwebtoken.JwtParser JwtParser} when the values
* embedded in the JWS need to be used to determine the <code>signing key</code> used to sign the JWS.
* *
* <p>A {@code SigningKeyResolver} is necessary when the signing key is not already known before parsing the JWT and the
* JWT header or payload (plaintext body or Claims) must be inspected first to determine how to look up the signing key.
* Once returned by the resolver, the JwtParser will then verify the JWS signature with the returned key. For
* example:</p>
*
* <pre>
* Jws&lt;Claims&gt; jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
* &#64;Override
* public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
* //inspect the header or claims, lookup and return the signing key
* return getSigningKeyBytes(header, claims); //implement me
* }})
* .parseClaimsJws(compact);
* </pre>
*
* <p>A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.</p>
*
* <h4>SigningKeyResolverAdapter</h4>
*
* <p>If you only need to resolve a signing key for a particular JWS (either a plaintext or Claims JWS), consider using
* the {@link io.jsonwebtoken.SigningKeyResolverAdapter} and overriding only the method you need to support instead of
* implementing this interface directly.</p>
*
* @see io.jsonwebtoken.SigningKeyResolverAdapter
* @since 0.4 * @since 0.4
*/ */
public interface SigningKeyResolver { public interface SigningKeyResolver {
/** /**
* This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} parsed a {@link Jws} and needs * Returns the signing key that should be used to validate a digital signature for the Claims JWS with the specified
* to resolve the signing key, based on a value embedded in the {@link JwsHeader} and/or the {@link Claims} * header and claims.
* <p/>
* <p>This method will only be invoked if an implementation is provided.</p>
* <p/>
* <p>Note that this key <em>MUST</em> be a valid key for the signature algorithm found in the JWT header
* (as the {@code alg} header parameter).</p>
* *
* @param header the parsed {@link JwsHeader} * @param header the header of the JWS to validate
* @param claims the parsed {@link Claims} * @param claims the claims (body) of the JWS to validate
* @return any object to be used after inspecting the JWS, or {@code null} if no return value is necessary. * @return the signing key that should be used to validate a digital signature for the Claims JWS with the specified
* header and claims.
*/ */
byte[] resolveSigningKey(JwsHeader header, Claims claims); Key resolveSigningKey(JwsHeader header, Claims claims);
/** /**
* This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} parsed a {@link Jws} and needs * Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the
* to resolve the signing key, based on a value embedded in the {@link JwsHeader} and/or the plaintext payload. * specified header and plaintext payload.
* <p/>
* <p>This method will only be invoked if an implementation is provided.</p>
* <p/>
* <p>Note that this key <em>MUST</em> be a valid key for the signature algorithm found in the JWT header
* (as the {@code alg} header parameter).</p>
* *
* @param header the parsed {@link JwsHeader} * @param header the header of the JWS to validate
* @param payload the jws plaintext payload. * @param plaintext the plaintext body of the JWS to validate
* @return any object to be used after inspecting the JWS, or {@code null} if no return value is necessary. * @return the signing key that should be used to validate a digital signature for the Plaintext JWS with the
* specified header and plaintext payload.
*/ */
byte[] resolveSigningKey(JwsHeader header, String payload); Key resolveSigningKey(JwsHeader header, String plaintext);
} }

View File

@ -15,27 +15,82 @@
*/ */
package io.jsonwebtoken; package io.jsonwebtoken;
import io.jsonwebtoken.lang.Assert;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
/** /**
* 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
* {@link SigningKeyResolver} interface that allows subclasses to process only the type of Jws body that * {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that
* are known/expected for a particular case. * is known/expected for a particular case.
* *
* <p>All of the methods in this implementation throw exceptions: overridden methods represent * <p>The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, String)} method
* scenarios expected by calling code in known situations. It would be unexpected to receive a JWS or JWT that did * implementations delegate to the
* not match parsing expectations, so all non-overridden methods throw exceptions to indicate that the JWT * {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, String)} methods
* input was unexpected.</p> * respectively. The latter two methods simply throw exceptions: they represent scenarios expected by
* calling code in known situations, and it is expected that you override the implementation in those known situations;
* non-overridden *KeyBytes methods indicates that the JWS input was unexpected.</p>
*
* <p>If either {@link #resolveSigningKey(JwsHeader, String)} or {@link #resolveSigningKey(JwsHeader, String)}
* are not overridden, one (or both) of the *KeyBytes variants must be overridden depending on your expected
* use case. You do not have to override any method that does not represent an expected condition.</p>
* *
* @since 0.4 * @since 0.4
*/ */
public class SigningKeyResolverAdapter implements SigningKeyResolver { public class SigningKeyResolverAdapter implements SigningKeyResolver {
@Override @Override
public byte[] resolveSigningKey(JwsHeader header, Claims claims) { public Key resolveSigningKey(JwsHeader header, Claims claims) {
throw new UnsupportedJwtException("Resolving signing keys with claims are not supported."); SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
byte[] keyBytes = resolveSigningKeyBytes(header, claims);
Assert.isTrue(!alg.isRsa(), "resolveSigningKeyBytes(JwsHeader, Claims) cannot be used for RSA signatures. " +
"Override the resolveSigningKey(JwsHeader, Claims) method instead and return a " +
"PublicKey or PrivateKey instance.");
return new SecretKeySpec(keyBytes, alg.getJcaName());
} }
@Override @Override
public byte[] resolveSigningKey(JwsHeader header, String payload) { public Key resolveSigningKey(JwsHeader header, String plaintext) {
throw new UnsupportedJwtException("Resolving signing keys with plaintext payload are not supported."); SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
byte[] keyBytes = resolveSigningKeyBytes(header, plaintext);
Assert.isTrue(!alg.isRsa(), "resolveSigningKeyBytes(JwsHeader, String) cannot be used for RSA signatures. " +
"Override the resolveSigningKey(JwsHeader, String) method instead and return a " +
"PublicKey or PrivateKey instance.");
return new SecretKeySpec(keyBytes, alg.getJcaName());
}
/**
* 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, </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 byte[] resolveSigningKeyBytes(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) or " +
"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
* 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 byte[] resolveSigningKeyBytes(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) or " +
"resolveSigningKeyBytes(JwsHeader, String) method.");
} }
} }

View File

@ -81,7 +81,7 @@ public class DefaultJwtParser implements JwtParser {
@Override @Override
public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) { public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) {
Assert.notNull(signingKeyResolver, "jwsSigningKeyResolver cannot be null."); Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null.");
this.signingKeyResolver = signingKeyResolver; this.signingKeyResolver = signingKeyResolver;
return this; return this;
} }
@ -246,7 +246,7 @@ public class DefaultJwtParser implements JwtParser {
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 ((key != null || keyBytes != null) && signingKeyResolver != null) {
String object = key != null ? "a key object" : "key bytes"; String object = key != null ? "a key object" : "key bytes";
throw new IllegalStateException("A signing key resolver object 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:
@ -258,9 +258,9 @@ public class DefaultJwtParser implements JwtParser {
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) { if (claims != null) {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, claims); key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else { } else {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, payload); key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
} }
} }

View File

@ -516,7 +516,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) { byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return key return key
} }
} }
@ -537,7 +537,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) { byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey() return randomKey()
} }
} }
@ -561,7 +561,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) { byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey() return randomKey()
} }
} }
@ -570,7 +570,7 @@ class JwtParserTest {
Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail() fail()
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
assertEquals ise.getMessage(), 'A signing key resolver object and a key object cannot both be specified. Choose either.' assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.'
} }
} }
@ -585,7 +585,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) { byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey() return randomKey()
} }
} }
@ -594,7 +594,7 @@ class JwtParserTest {
Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail() fail()
} catch (IllegalStateException ise) { } catch (IllegalStateException ise) {
assertEquals ise.getMessage(), 'A signing key resolver object and key bytes cannot both be specified. Choose either.' assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.'
} }
} }
@ -611,7 +611,7 @@ class JwtParserTest {
Jwts.parser().setSigningKeyResolver(null).parseClaimsJws(compact); Jwts.parser().setSigningKeyResolver(null).parseClaimsJws(compact);
fail() fail()
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
assertEquals iae.getMessage(), 'jwsSigningKeyResolver cannot be null.' assertEquals iae.getMessage(), 'SigningKeyResolver cannot be null.'
} }
} }
@ -630,7 +630,9 @@ class JwtParserTest {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail() fail()
} catch (UnsupportedJwtException ex) { } catch (UnsupportedJwtException ex) {
assertEquals ex.getMessage(), 'Resolving signing keys with claims are not supported.' assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' +
'Claims JWS signing key resolution. Consider overriding either the ' +
'resolveSigningKey(JwsHeader, Claims) or resolveSigningKeyBytes(JwsHeader, Claims) method.'
} }
} }
@ -649,7 +651,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, String payload) { byte[] resolveSigningKeyBytes(JwsHeader header, String payload) {
return key return key
} }
} }
@ -670,7 +672,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() { def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override @Override
byte[] resolveSigningKey(JwsHeader header, String payload) { byte[] resolveSigningKeyBytes(JwsHeader header, String payload) {
return randomKey() return randomKey()
} }
} }
@ -698,7 +700,9 @@ class JwtParserTest {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact); Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact);
fail() fail()
} catch (UnsupportedJwtException ex) { } catch (UnsupportedJwtException ex) {
assertEquals ex.getMessage(), 'Resolving signing keys with plaintext payload are not supported.' assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' +
'JWS signing key resolution. Consider overriding either the ' +
'resolveSigningKey(JwsHeader, String) or resolveSigningKeyBytes(JwsHeader, String) method.'
} }
} }
} }