diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 00bd706a..f4b190ba 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -78,17 +78,32 @@ public interface JwtParser { JwtParser setSigningKey(Key key); /** - * Sets the {@link SigningKeyResolver} used to resolve the signing key 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. - *

- *

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.

- *

- *

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. - *

+ * Sets the {@link SigningKeyResolver} used to acquire the signing key 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. + * + *

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:

+ * + *
+     * Jws<Claims> jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
+     *         @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);
+     * 
+ * + *

A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.

+ * + *

This method should only be used if a signing key is not provided by the other {@code setSigningKey*} builder + * methods.

* * @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. * - *

Note that if you are reasonably sure that the token is signed, it is usually more efficient to attempt to + *

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.

* * @param jwt the compact serialized JWT to check diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java index 7e469b83..b068db9d 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolver.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolver.java @@ -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. - *

- * 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 signing key 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. * + *

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:

+ * + *
+ * Jws<Claims> jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
+ *         @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);
+ * 
+ * + *

A {@code SigningKeyResolver} is invoked once during parsing before the signature is verified.

+ * + *

SigningKeyResolverAdapter

+ * + *

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.

+ * + * @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} - *

- *

This method will only be invoked if an implementation is provided.

- *

- *

Note that this key MUST be a valid key for the signature algorithm found in the JWT header - * (as the {@code alg} header parameter).

+ * 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. - *

- *

This method will only be invoked if an implementation is provided.

- *

- *

Note that this key MUST be a valid key for the signature algorithm found in the JWT header - * (as the {@code alg} header parameter).

+ * 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); } diff --git a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java index f4af8580..46d6c5e8 100644 --- a/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java +++ b/src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java @@ -15,27 +15,82 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.lang.Assert; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; + /** * An Adapter 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. * - *

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.

+ *

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.

+ * + *

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.

* * @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. + * + *

NOTE: You cannot override this method when validating RSA signatures. If you expect RSA signatures,

+ * + * @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."); } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 468f8dd7..040d9125 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -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); } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 43ced211..510bc405 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -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.' } } }