Skip to content

Commit

Permalink
Merge pull request #9 from josebarrueta/master
Browse files Browse the repository at this point in the history
Issue #8 Add ability to resolve signing key based on Jws embedded values...
  • Loading branch information
lhazlewood committed Nov 20, 2014
2 parents 3e35e4e + 44965c7 commit f596844
Show file tree
Hide file tree
Showing 5 changed files with 338 additions and 1 deletion.
15 changes: 15 additions & 0 deletions src/main/java/io/jsonwebtoken/JwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ 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>
*
* @param signingKeyResolver the signing key resolver used to retrieve the signing key.
* @return the parser for method chaining.
*/
JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver);

/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise.
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/io/jsonwebtoken/SigningKeyResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken;

/**
* 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.
*
* @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>
*
* @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.
*/
byte[] 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>
*
* @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.
*/
byte[] resolveSigningKey(JwsHeader header, String payload);
}
41 changes: 41 additions & 0 deletions src/main/java/io/jsonwebtoken/SigningKeyResolverAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken;

/**
* 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.
*
* <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>
*
* @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.");
}

@Override
public byte[] resolveSigningKey(JwsHeader header, String payload) {
throw new UnsupportedJwtException("Resolving signing keys with plaintext payload are not supported.");
}
}
25 changes: 24 additions & 1 deletion src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtHandler;
import io.jsonwebtoken.JwtHandlerAdapter;
Expand Down Expand Up @@ -55,6 +56,8 @@ public class DefaultJwtParser implements JwtParser {

private Key key;

private SigningKeyResolver signingKeyResolver;

@Override
public JwtParser setSigningKey(byte[] key) {
Assert.notEmpty(key, "signing key cannot be null or empty.");
Expand All @@ -76,6 +79,13 @@ public JwtParser setSigningKey(Key key) {
return this;
}

@Override
public JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver) {
Assert.notNull(signingKeyResolver, "jwsSigningKeyResolver cannot be null.");
this.signingKeyResolver = signingKeyResolver;
return this;
}

@Override
public boolean isSigned(String jwt) {

Expand Down Expand Up @@ -234,14 +244,27 @@ public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException,

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.");
}

//digitally signed, let's assert the signature:
Key key = this.key;

if (key == null) { //fall back to keyBytes

if (!Objects.isEmpty(this.keyBytes)) {
byte[] keyBytes = this.keyBytes;

if (Objects.isEmpty(keyBytes) && signingKeyResolver != null) { //use the signingKeyResolver
if (claims != null) {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, claims);
} else {
keyBytes = signingKeyResolver.resolveSigningKey(jwsHeader, payload);
}
}

if (!Objects.isEmpty(keyBytes)) {

Assert.isTrue(!algorithm.isRsa(),
"Key bytes cannot be specified for RSA signatures. Please specify a PublicKey or PrivateKey instance.");
Expand Down
200 changes: 200 additions & 0 deletions src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,204 @@ class JwtParserTest {
}
}

// ========================================================================
// parseClaimsJws with signingKey resolver.
// ========================================================================

@Test
void testParseClaimsWithSigningKeyResolver() {

String subject = 'Joe'

byte[] key = randomKey()

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
return key
}
}

Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact)

assertEquals jws.getBody().getSubject(), subject
}

@Test
void testParseClaimsWithSigningKeyResolverInvalidKey() {

String subject = 'Joe'

byte[] key = randomKey()

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
return randomKey()
}
}

try {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail()
} catch (SignatureException se) {
assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.'
}
}

@Test
void testParseClaimsWithSigningKeyResolverAndKey() {

String subject = 'Joe'

SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256");

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
return randomKey()
}
}

try {
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.'
}
}

@Test
void testParseClaimsWithSigningKeyResolverAndKeyBytes() {

String subject = 'Joe'

byte[] key = randomKey()

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, Claims claims) {
return randomKey()
}
}

try {
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.'
}
}

@Test
void testParseClaimsWithNullSigningKeyResolver() {

String subject = 'Joe'

byte[] key = randomKey()

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

try {
Jwts.parser().setSigningKeyResolver(null).parseClaimsJws(compact);
fail()
} catch (IllegalArgumentException iae) {
assertEquals iae.getMessage(), 'jwsSigningKeyResolver cannot be null.'
}
}

@Test
void testParseClaimsWithInvalidSigningKeyResolverAdapter() {

String subject = 'Joe'

byte[] key = randomKey()

String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter()

try {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact);
fail()
} catch (UnsupportedJwtException ex) {
assertEquals ex.getMessage(), 'Resolving signing keys with claims are not supported.'
}
}

// ========================================================================
// parsePlaintextJws with signingKey resolver.
// ========================================================================

@Test
void testParsePlaintextJwsWithSigningKeyResolverAdapter() {

String inputPayload = 'Hello world!'

byte[] key = randomKey()

String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, String payload) {
return key
}
}

Jws<String> jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact);

assertEquals jws.getBody(), inputPayload
}

@Test
void testParsePlaintextJwsWithSigningKeyResolverInvalidKey() {

String inputPayload = 'Hello world!'

byte[] key = randomKey()

String compact = Jwts.builder().setPayload(inputPayload).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
byte[] resolveSigningKey(JwsHeader header, String payload) {
return randomKey()
}
}

try {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact);
fail()
} catch (SignatureException se) {
assertEquals se.getMessage(), 'JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.'
}
}

@Test
void testParsePlaintextJwsWithInvalidSigningKeyResolverAdapter() {

String payload = 'Hello world!'

byte[] key = randomKey()

String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact()

def signingKeyResolver = new SigningKeyResolverAdapter()

try {
Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact);
fail()
} catch (UnsupportedJwtException ex) {
assertEquals ex.getMessage(), 'Resolving signing keys with plaintext payload are not supported.'
}
}
}

0 comments on commit f596844

Please sign in to comment.