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);
/**
* Sets the {@link SigningKeyResolver} used to resolve the <code>signing key</code> using the parsed {@link JwsHeader}
* and/or the {@link Claims}. If the specified JWT 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/>
* <p>This is a convenience method: the {@code jwsSignatureKeyResolver} is used after a Jws has been parsed and either the
* {@link JwsHeader} or the {@link Claims} embedded in the {@link Jws} can be used to resolve the signing key.
* </p>
* Sets the {@link SigningKeyResolver} used to acquire the <code>signing key</code> that should be used to verify
* a JWS's signature. If the parsed String is not a JWS (no signature), this resolver is not used.
*
* <p>Specifying 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 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.
* @return the parser for method chaining.
* @since 0.4
*/
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}
* 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>
*
* @param jwt the compact serialized JWT to check

View File

@ -15,44 +15,59 @@
*/
package io.jsonwebtoken;
import java.security.Key;
/**
* A JwsSigningKeyResolver is invoked by a {@link io.jsonwebtoken.JwtParser JwtParser} if it's provided and the
* JWT being parsed is signed.
* <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.
* A {@code SigningKeyResolver} can be used by a {@link io.jsonwebtoken.JwtParser JwtParser} to find a signing key that
* should be used to verify a JWS signature.
*
* <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
*/
public interface SigningKeyResolver {
/**
* This method is invoked when a {@link io.jsonwebtoken.JwtParser JwtParser} parsed a {@link Jws} and needs
* to resolve the signing key, based on a value embedded in the {@link JwsHeader} and/or the {@link 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>
* Returns the signing key that should be used to validate a digital signature for the Claims JWS with the specified
* header and claims.
*
* @param header the parsed {@link JwsHeader}
* @param claims the parsed {@link Claims}
* @return any object to be used after inspecting the JWS, or {@code null} if no return value is necessary.
* @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.
*/
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
* to resolve the signing key, based on a value embedded in the {@link JwsHeader} and/or the 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>
* Returns the signing key that should be used to validate a digital signature for the Plaintext JWS with the
* specified header and plaintext payload.
*
* @param header the parsed {@link JwsHeader}
* @param payload the jws plaintext payload.
* @return any object to be used after inspecting the JWS, or {@code null} if no return value is necessary.
* @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.
*/
byte[] resolveSigningKey(JwsHeader header, String payload);
Key resolveSigningKey(JwsHeader header, String plaintext);
}

View File

@ -15,27 +15,82 @@
*/
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
* {@link SigningKeyResolver} interface that allows subclasses to process only the type of Jws body that
* are known/expected for a particular case.
* {@link SigningKeyResolver} interface that allows subclasses to process only the type of JWS body that
* is known/expected for a particular case.
*
* <p>All of the methods in this implementation throw exceptions: overridden methods represent
* scenarios expected by calling code in known situations. It would be unexpected to receive a JWS or JWT that did
* not match parsing expectations, so all non-overridden methods throw exceptions to indicate that the JWT
* input was unexpected.</p>
* <p>The {@link #resolveSigningKey(JwsHeader, Claims)} and {@link #resolveSigningKey(JwsHeader, String)} method
* implementations delegate to the
* {@link #resolveSigningKeyBytes(JwsHeader, Claims)} and {@link #resolveSigningKeyBytes(JwsHeader, String)} methods
* 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
*/
public class SigningKeyResolverAdapter implements SigningKeyResolver {
@Override
public byte[] resolveSigningKey(JwsHeader header, Claims claims) {
throw new UnsupportedJwtException("Resolving signing keys with claims are not supported.");
public Key resolveSigningKey(JwsHeader header, Claims claims) {
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
public byte[] resolveSigningKey(JwsHeader header, String payload) {
throw new UnsupportedJwtException("Resolving signing keys with plaintext payload are not supported.");
public Key resolveSigningKey(JwsHeader header, String plaintext) {
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
public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) {
Assert.notNull(signingKeyResolver, "jwsSigningKeyResolver cannot be null.");
Assert.notNull(signingKeyResolver, "SigningKeyResolver cannot be null.");
this.signingKeyResolver = signingKeyResolver;
return this;
}
@ -245,8 +245,8 @@ public class DefaultJwtParser implements JwtParser {
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 object and" + object + "cannot both be specified. Choose either.");
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:
@ -258,9 +258,9 @@ public class DefaultJwtParser implements JwtParser {
if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
key = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
key = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
}
}

View File

@ -516,7 +516,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return key
}
}
@ -537,7 +537,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey()
}
}
@ -561,7 +561,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey()
}
}
@ -570,7 +570,7 @@ class JwtParserTest {
Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail()
} 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() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return randomKey()
}
}
@ -594,7 +594,7 @@ class JwtParserTest {
Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail()
} 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);
fail()
} 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);
fail()
} 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() {
@Override
byte[] resolveSigningKey(JwsHeader header, String payload) {
byte[] resolveSigningKeyBytes(JwsHeader header, String payload) {
return key
}
}
@ -670,7 +672,7 @@ class JwtParserTest {
def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, String payload) {
byte[] resolveSigningKeyBytes(JwsHeader header, String payload) {
return randomKey()
}
}
@ -698,7 +700,9 @@ class JwtParserTest {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact);
fail()
} 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.'
}
}
}