From 73d4e788ef533d1544ee9a949bd44287a35933cb Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Wed, 26 Aug 2015 16:59:55 -0700 Subject: [PATCH 01/50] Issue 39 - Add generic body type to the DefaultJws implementation. --- src/main/java/io/jsonwebtoken/impl/DefaultJws.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index f8298a89..f1b4958b 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -18,7 +18,7 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; -public class DefaultJws implements Jws { +public class DefaultJws implements Jws { private final JwsHeader header; private final B body; From 5c664358251b6fae123d61120a5d221ef8c3071f Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Wed, 26 Aug 2015 17:07:41 -0700 Subject: [PATCH 02/50] Issue-34 getting rid of slf4j. --- pom.xml | 7 ------ .../java/io/jsonwebtoken/lang/Classes.java | 24 ++----------------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index 34923813..3cd8715f 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,6 @@ UTF-8 ${user.name}-${maven.build.timestamp} - 1.7.6 2.4.2 @@ -78,12 +77,6 @@ - - - org.slf4j - slf4j-api - ${slf4j.version} - com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/io/jsonwebtoken/lang/Classes.java b/src/main/java/io/jsonwebtoken/lang/Classes.java index b8a62e1b..e031d792 100644 --- a/src/main/java/io/jsonwebtoken/lang/Classes.java +++ b/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -15,9 +15,6 @@ */ package io.jsonwebtoken.lang; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.InputStream; import java.lang.reflect.Constructor; @@ -26,11 +23,6 @@ import java.lang.reflect.Constructor; */ public class Classes { - /** - * Private internal log instance. - */ - private static final Logger log = LoggerFactory.getLogger(Classes.class); - /** * @since 0.1 */ @@ -78,18 +70,10 @@ public class Classes { Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { - if (log.isTraceEnabled()) { - log.trace("Unable to load class named [" + fqcn + - "] from the thread context ClassLoader. Trying the current ClassLoader..."); - } clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { - if (log.isTraceEnabled()) { - log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + - "Trying the system/application ClassLoader..."); - } clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); } @@ -212,9 +196,7 @@ public class Classes { try { clazz = cl.loadClass(fqcn); } catch (ClassNotFoundException e) { - if (log.isTraceEnabled()) { - log.trace("Unable to load clazz named [" + fqcn + "] from class loader [" + cl + "]"); - } + //Class couldn't be found by loader } } return clazz; @@ -233,9 +215,7 @@ public class Classes { try { return doGetClassLoader(); } catch (Throwable t) { - if (log.isDebugEnabled()) { - log.debug("Unable to acquire ClassLoader.", t); - } + //Unable to get ClassLoader } return null; } From 59993cf61438287ffce327e62be51038a5193ab7 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 9 Sep 2015 22:50:28 -0400 Subject: [PATCH 03/50] Fixed broken testParseClaimsJwsWithPlaintextJws test. --- .../io/jsonwebtoken/JwtParserTest.groovy | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 490099e8..f5734048 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -167,7 +167,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact(); try { - Jwts.parser().parse(compact); + Jwts.parser().parse(compact) + fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') } @@ -181,7 +182,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact(); try { - Jwts.parser().parse(compact); + Jwts.parser().parse(compact) + fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') } @@ -268,7 +270,8 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().parseClaimsJwt(compact); + Jwts.parser().parseClaimsJwt(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' } @@ -327,7 +330,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact(); try { - Jwts.parser().parseClaimsJwt(compact); + Jwts.parser().parseClaimsJwt(compact) + fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') } @@ -361,7 +365,8 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' } @@ -377,7 +382,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' } @@ -393,7 +399,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' } @@ -452,7 +459,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(sub).setNotBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact(); try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') assertEquals e.getClaims().getSubject(), sub @@ -470,7 +478,8 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned plaintext JWTs are not supported.' } @@ -486,7 +495,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' } @@ -502,7 +512,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed Claims JWSs are not supported.' } From bdb7597f0ae4dcf307b823d9b15b845537d777f0 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 9 Sep 2015 23:22:17 -0400 Subject: [PATCH 04/50] Got rid of semicolons in JwtParserTest to make it consistent throughout. --- .../io/jsonwebtoken/JwtParserTest.groovy | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index f5734048..a831c4b1 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -29,17 +29,17 @@ class JwtParserTest { protected static byte[] randomKey() { //create random signing key for testing: - byte[] key = new byte[64]; - random.nextBytes(key); - return key; + byte[] key = new byte[64] + random.nextBytes(key) + return key } @Test void testSetDuplicateSigningKeys() { - byte[] keyBytes = randomKey(); + byte[] keyBytes = randomKey() - SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256"); + SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA256") String compact = Jwts.builder().setPayload('Hello World!').signWith(SignatureAlgorithm.HS256, keyBytes).compact() @@ -58,7 +58,7 @@ class JwtParserTest { @Test void testIsSignedWithJunkArgument() { - assertFalse Jwts.parser().isSigned('hello'); + assertFalse Jwts.parser().isSigned('hello') } @Test @@ -67,10 +67,10 @@ class JwtParserTest { String junkPayload = '{;aklsjd;fkajsd;fkjasd;lfkj}' String bad = TextCodec.BASE64.encode('{"alg":"none"}') + '.' + - TextCodec.BASE64.encode(junkPayload) + '.'; + TextCodec.BASE64.encode(junkPayload) + '.' try { - Jwts.parser().parse(bad); + Jwts.parser().parse(bad) fail() } catch (MalformedJwtException expected) { assertEquals expected.getMessage(), 'Unable to read JSON value: ' + junkPayload @@ -82,7 +82,7 @@ class JwtParserTest { String badAlgorithmName = 'whatever' - String header = "{\"alg\":\"$badAlgorithmName\"}"; + String header = "{\"alg\":\"$badAlgorithmName\"}" String payload = '{"subject":"Joe"}' @@ -90,10 +90,10 @@ class JwtParserTest { String bad = TextCodec.BASE64.encode(header) + '.' + TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig); + TextCodec.BASE64.encode(badSig) try { - Jwts.parser().setSigningKey(randomKey()).parse(bad); + Jwts.parser().setSigningKey(randomKey()).parse(bad) fail() } catch (SignatureException se) { assertEquals se.getMessage(), "Unsupported signature algorithm '$badAlgorithmName'".toString() @@ -111,10 +111,10 @@ class JwtParserTest { String bad = TextCodec.BASE64.encode(header) + '.' + TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig); + TextCodec.BASE64.encode(badSig) try { - Jwts.parser().setSigningKey(randomKey()).parse(bad); + Jwts.parser().setSigningKey(randomKey()).parse(bad) 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.' @@ -133,10 +133,10 @@ class JwtParserTest { String bad = TextCodec.BASE64.encode(header) + '.' + TextCodec.BASE64.encode(payload) + '.' + - TextCodec.BASE64.encode(badSig); + TextCodec.BASE64.encode(badSig) try { - Jwts.parser().setSigningKey(randomKey()).parse(bad); + Jwts.parser().setSigningKey(randomKey()).parse(bad) fail() } catch (MalformedJwtException se) { assertEquals se.getMessage(), 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' @@ -146,8 +146,8 @@ class JwtParserTest { @Test void testParseWithBase64EncodedSigningKey() { - byte[] key = randomKey(); - String base64Encodedkey = TextCodec.BASE64.encode(key); + byte[] key = randomKey() + String base64Encodedkey = TextCodec.BASE64.encode(key) String payload = 'Hello world!' String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, base64Encodedkey).compact() @@ -162,9 +162,9 @@ class JwtParserTest { @Test void testParseWithExpiredJwt() { - Date exp = new Date(System.currentTimeMillis() - 1000); + Date exp = new Date(System.currentTimeMillis() - 1000) - String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact(); + String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { Jwts.parser().parse(compact) @@ -177,9 +177,9 @@ class JwtParserTest { @Test void testParseWithPrematureJwt() { - Date nbf = new Date(System.currentTimeMillis() + 100000); + Date nbf = new Date(System.currentTimeMillis() + 100000) - String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact(); + String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { Jwts.parser().parse(compact) @@ -200,7 +200,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).compact() - Jwt jwt = Jwts.parser().parsePlaintextJwt(compact); + Jwt jwt = Jwts.parser().parsePlaintextJwt(compact) assertEquals jwt.getBody(), payload } @@ -208,11 +208,11 @@ class JwtParserTest { @Test void testParsePlaintextJwtWithClaimsJwt() { - String compact = Jwts.builder().setSubject('Joe').compact(); + String compact = Jwts.builder().setSubject('Joe').compact() try { - Jwts.parser().parsePlaintextJwt(compact); - fail(); + Jwts.parser().parsePlaintextJwt(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Unsigned Claims JWTs are not supported.' } @@ -226,8 +226,8 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact); - fail(); + Jwts.parser().parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed JWSs are not supported.' } @@ -239,8 +239,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parsePlaintextJws(compact); - fail(); + Jwts.parser().parsePlaintextJws(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed JWSs are not supported.' } @@ -257,7 +257,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).compact() - Jwt jwt = Jwts.parser().parseClaimsJwt(compact); + Jwt jwt = Jwts.parser().parseClaimsJwt(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -285,8 +285,8 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parseClaimsJwt(compact); - fail(); + Jwts.parser().parseClaimsJwt(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed JWSs are not supported.' } @@ -298,8 +298,8 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').signWith(SignatureAlgorithm.HS256, randomKey()).compact() try { - Jwts.parser().parseClaimsJwt(compact); - fail(); + Jwts.parser().parseClaimsJwt(compact) + fail() } catch (UnsupportedJwtException e) { assertEquals e.getMessage(), 'Signed JWSs are not supported.' } @@ -308,15 +308,15 @@ class JwtParserTest { @Test void testParseClaimsJwtWithExpiredJwt() { - long nowMillis = System.currentTimeMillis(); + long nowMillis = System.currentTimeMillis() //some time in the past: - Date exp = new Date(nowMillis - 1000); + Date exp = new Date(nowMillis - 1000) String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().parseClaimsJwt(compact); - fail(); + Jwts.parser().parseClaimsJwt(compact) + fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') } @@ -325,9 +325,9 @@ class JwtParserTest { @Test void testParseClaimsJwtWithPrematureJwt() { - Date nbf = new Date(System.currentTimeMillis() + 100000); + Date nbf = new Date(System.currentTimeMillis() + 100000) - String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact(); + String compact = Jwts.builder().setSubject('Joe').setNotBefore(nbf).compact() try { Jwts.parser().parseClaimsJwt(compact) @@ -350,7 +350,7 @@ class JwtParserTest { String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key).compact() - Jwt jwt = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + Jwt jwt = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) assertEquals jwt.getBody(), payload } @@ -419,7 +419,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(sub).signWith(SignatureAlgorithm.HS256, key).compact() - Jwt jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + Jwt jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) assertEquals jwt.getBody().getSubject(), sub } @@ -431,15 +431,15 @@ class JwtParserTest { byte[] key = randomKey() - long nowMillis = System.currentTimeMillis(); + long nowMillis = System.currentTimeMillis() //some time in the past: - Date exp = new Date(nowMillis - 1000); + Date exp = new Date(nowMillis - 1000) String compact = Jwts.builder().setSubject(sub).signWith(SignatureAlgorithm.HS256, key).setExpiration(exp).compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJwt(compact); - fail(); + Jwts.parser().setSigningKey(key).parseClaimsJwt(compact) + fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') assertEquals e.getClaims().getSubject(), sub @@ -454,9 +454,9 @@ class JwtParserTest { byte[] key = randomKey() - Date nbf = new Date(System.currentTimeMillis() + 100000); + Date nbf = new Date(System.currentTimeMillis() + 100000) - String compact = Jwts.builder().setSubject(sub).setNotBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact(); + String compact = Jwts.builder().setSubject(sub).setNotBefore(nbf).signWith(SignatureAlgorithm.HS256, key).compact() try { Jwts.parser().setSigningKey(key).parseClaimsJws(compact) @@ -561,7 +561,7 @@ class JwtParserTest { } try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); + 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.' @@ -573,7 +573,7 @@ class JwtParserTest { String subject = 'Joe' - SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256"); + SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256") String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() @@ -585,7 +585,7 @@ class JwtParserTest { } try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) fail() } catch (IllegalStateException ise) { assertEquals ise.getMessage(), 'A signing key resolver and a key object cannot both be specified. Choose either.' @@ -609,7 +609,7 @@ class JwtParserTest { } try { - Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); + Jwts.parser().setSigningKey(key).setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) fail() } catch (IllegalStateException ise) { assertEquals ise.getMessage(), 'A signing key resolver and key bytes cannot both be specified. Choose either.' @@ -626,7 +626,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact() try { - Jwts.parser().setSigningKeyResolver(null).parseClaimsJws(compact); + Jwts.parser().setSigningKeyResolver(null).parseClaimsJws(compact) fail() } catch (IllegalArgumentException iae) { assertEquals iae.getMessage(), 'SigningKeyResolver cannot be null.' @@ -645,7 +645,7 @@ class JwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact); + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parseClaimsJws(compact) fail() } catch (UnsupportedJwtException ex) { assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support ' + @@ -674,7 +674,7 @@ class JwtParserTest { } } - Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact); + Jws jws = Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) assertEquals jws.getBody(), inputPayload } @@ -696,7 +696,7 @@ class JwtParserTest { } try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact); + 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.' @@ -715,7 +715,7 @@ class JwtParserTest { def signingKeyResolver = new SigningKeyResolverAdapter() try { - Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact); + Jwts.parser().setSigningKeyResolver(signingKeyResolver).parsePlaintextJws(compact) fail() } catch (UnsupportedJwtException ex) { assertEquals ex.getMessage(), 'The specified SigningKeyResolver implementation does not support plaintext ' + From 16b970d28efa6f0b91428cf58fb70e3746ca44ed Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 9 Sep 2015 21:14:59 -0700 Subject: [PATCH 05/50] Fixes #35. Also enhanced some test code coverage. --- pom.xml | 5 -- .../impl/crypto/MacValidator.java | 3 +- .../impl/Base64UrlCodecTest.groovy | 19 ++++++ .../impl/DefaultJwtBuilderTest.groovy | 61 +++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy diff --git a/pom.xml b/pom.xml index 3cd8715f..057dea65 100644 --- a/pom.xml +++ b/pom.xml @@ -296,11 +296,6 @@ 96 100 - - io.jsonwebtoken.impl.Base64UrlCodec - 100 - 95 - io.jsonwebtoken.impl.DefaultJwtBuilder 91 diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java index 95227ae9..af319666 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; import java.security.Key; +import java.security.MessageDigest; import java.util.Arrays; public class MacValidator implements SignatureValidator { @@ -31,6 +32,6 @@ public class MacValidator implements SignatureValidator { @Override public boolean isValid(byte[] data, byte[] signature) { byte[] computed = this.signer.sign(data); - return Arrays.equals(computed, signature); + return MessageDigest.isEqual(computed, signature); } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy new file mode 100644 index 00000000..45969d86 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/Base64UrlCodecTest.groovy @@ -0,0 +1,19 @@ +package io.jsonwebtoken.impl + +import org.junit.Test +import static org.junit.Assert.* + +class Base64UrlCodecTest { + + @Test + void testRemovePaddingWithEmptyByteArray() { + + def codec = new Base64UrlCodec() + + byte[] empty = new byte[0]; + + def result = codec.removePadding(empty) + + assertSame empty, result + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 4dab7d7c..7090d03c 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -119,6 +119,17 @@ class DefaultJwtBuilderTest { } } + @Test + void testCompactWithoutPayloadOrClaims() { + def b = new DefaultJwtBuilder() + try { + b.compact() + fail() + } catch (IllegalStateException ise) { + assertEquals ise.message, "Either 'payload' or 'claims' must be specified." + } + } + @Test void testCompactWithBothPayloadAndClaims() { def b = new DefaultJwtBuilder() @@ -197,4 +208,54 @@ class DefaultJwtBuilderTest { } } + + @Test + void testSetHeaderParamsWithNullMap() { + def b = new DefaultJwtBuilder() + b.setHeaderParams(null) + assertNull b.header + } + + @Test + void testSetHeaderParamsWithEmptyMap() { + def b = new DefaultJwtBuilder() + b.setHeaderParams([:]) + assertNull b.header + } + + @Test + void testSetIssuerWithNull() { + def b = new DefaultJwtBuilder() + b.setIssuer(null) + assertNull b.claims + } + + @Test + void testSetSubjectWithNull() { + def b = new DefaultJwtBuilder() + b.setSubject(null) + assertNull b.claims + } + + @Test + void testSetAudienceWithNull() { + def b = new DefaultJwtBuilder() + b.setAudience(null) + assertNull b.claims + } + + @Test + void testSetIdWithNull() { + def b = new DefaultJwtBuilder() + b.setId(null) + assertNull b.claims + } + + @Test + void testClaimNullValue() { + def b = new DefaultJwtBuilder() + b.claim('foo', null) + assertNull b.claims + } + } From 56fb2c6a75b32433dae2b4e1e3cbf34471862438 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Fri, 11 Sep 2015 11:28:43 -0700 Subject: [PATCH 06/50] Updated example to use parseClaimsJws to reflect most common use case --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d09f55e2..f0a863a8 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ But what if signature validation failed? You can catch `SignatureException` and ```java try { - Jwts.parser().setSigningKey(key).parse(compactJwt); + Jwts.parser().setSigningKey(key).parseClaimsJws(compactJwt); //OK, we can trust this JWT From 8f49666a40abc73b57ffb998f56750a6a7744c17 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 01:09:44 -0400 Subject: [PATCH 07/50] Enforce arbitrary claim expectations when parsing --- README.md | 8 +- .../io/jsonwebtoken/ClaimJwtException.java | 3 + .../jsonwebtoken/IncorrectClaimException.java | 32 +++++ .../jsonwebtoken/InvalidClaimException.java | 50 +++++++ src/main/java/io/jsonwebtoken/JwtParser.java | 17 +++ .../jsonwebtoken/MissingClaimException.java | 32 +++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 47 +++++++ .../IncorrectClaimExceptionTest.groovy | 46 +++++++ .../InvalidClaimExceptionTest.groovy | 46 +++++++ .../io/jsonwebtoken/JwtParserTest.groovy | 125 ++++++++++++++++++ .../MissingClaimExceptionTest.groovy | 46 +++++++ 11 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/IncorrectClaimException.java create mode 100644 src/main/java/io/jsonwebtoken/InvalidClaimException.java create mode 100644 src/main/java/io/jsonwebtoken/MissingClaimException.java create mode 100644 src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy diff --git a/README.md b/README.md index d09f55e2..d49c82b6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Maven: io.jsonwebtoken jjwt - 0.5.1 + 0.6 ``` @@ -24,7 +24,7 @@ Gradle: ```groovy dependencies { - compile 'io.jsonwebtoken:jjwt:0.5.1' + compile 'io.jsonwebtoken:jjwt:0.6' } ``` @@ -99,6 +99,10 @@ These feature sets will be implemented in a future release when possible. Commu ## Release Notes +### 0.6 + +- Added the ability to set expectations when parsing a JWT which enforces a particular claim having a particular value + ### 0.5.1 - Minor [bug](https://github.com/jwtk/jjwt/issues/31) fix [release](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5.1+is%3Aclosed) that ensures correct Base64 padding in Android runtimes. diff --git a/src/main/java/io/jsonwebtoken/ClaimJwtException.java b/src/main/java/io/jsonwebtoken/ClaimJwtException.java index 89059b2b..bb7c81fd 100644 --- a/src/main/java/io/jsonwebtoken/ClaimJwtException.java +++ b/src/main/java/io/jsonwebtoken/ClaimJwtException.java @@ -22,6 +22,9 @@ package io.jsonwebtoken; */ public abstract class ClaimJwtException extends JwtException { + public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not present in the JWT claims."; + private final Header header; private final Claims claims; diff --git a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java new file mode 100644 index 00000000..bf63cd5a --- /dev/null +++ b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 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; + +/** + * IncorrectClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an + * expected claim has a value that is not expected. + * + * @since 0.6 + */ +public class IncorrectClaimException extends InvalidClaimException { + public IncorrectClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + public IncorrectClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/src/main/java/io/jsonwebtoken/InvalidClaimException.java new file mode 100644 index 00000000..9706a0b3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2015 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; + +/** + * InvalidClaimException is a subclass of the {@link ClaimJwtException} that is thrown after a validation of an JTW claim failed. + * + * @since 0.6 + */ +public class InvalidClaimException extends ClaimJwtException { + private String claimName; + private Object claimValue; + + protected InvalidClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + protected InvalidClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } + + public String getClaimName() { + return claimName; + } + + public void setClaimName(String claimName) { + this.claimName = claimName; + } + + public Object getClaimValue() { + return claimValue; + } + + public void setClaimValue(Object claimValue) { + this.claimValue = claimValue; + } +} diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f4b190ba..c26da433 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -26,6 +26,23 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for any given claim name. + * + * If an expectation is set for a particular claim name and the JWT being parsed does not have that claim set, + * a {@Link MissingClaimException} will be thrown. + * + * If an expectation is set for a particular claim name and the JWT being parsed has a value that is different than + * the expected value, a {@link IncorrectClaimException} will be thrown. + * + * If either {@code claimName} is null or empty or {@code value} is null, the expectation is simply ignored. + * + * @param claimName + * @param value + * @return the parser for method chaining. + */ + JwtParser expect(String claimName, Object value); + /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. diff --git a/src/main/java/io/jsonwebtoken/MissingClaimException.java b/src/main/java/io/jsonwebtoken/MissingClaimException.java new file mode 100644 index 00000000..d453df57 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 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; + +/** + * MissingClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an + * expected claim is missing. + * + * @since 0.6 + */ +public class MissingClaimException extends InvalidClaimException { + public MissingClaimException(Header header, Claims claims, String message) { + super(header, claims, message); + } + + public MissingClaimException(Header header, Claims claims, String message, Throwable cause) { + super(header, claims, message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ba888e04..36142292 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -16,11 +16,15 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ClaimJwtException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; +import io.jsonwebtoken.IncorrectClaimException; +import io.jsonwebtoken.InvalidClaimException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtHandler; @@ -42,6 +46,7 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; @SuppressWarnings("unchecked") @@ -58,6 +63,17 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; + Map expectedClaims = new LinkedHashMap(); + + @Override + public JwtParser expect(String claimName, Object value) { + if (claimName != null && claimName.length() > 0 && value != null) { + expectedClaims.put(claimName, value); + } + + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -298,6 +314,8 @@ public class DefaultJwtParser implements JwtParser { throw new PrematureJwtException(header, claims, msg); } } + + validateExpectedClaims(header, claims); } Object body = claims != null ? claims : payload; @@ -309,6 +327,35 @@ public class DefaultJwtParser implements JwtParser { } } + private void validateExpectedClaims(Header header, Claims claims) { + for (String expectedClaimName : expectedClaims.keySet()) { + Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object actualClaimValue = claims.get(expectedClaimName); + InvalidClaimException invalidClaimException = null; + + if (actualClaimValue == null) { + String msg = String.format( + ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue + ); + invalidClaimException = new MissingClaimException(header, claims, msg); + } + else if (!expectedClaimValue.equals(actualClaimValue)) { + String msg = String.format( + ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, + expectedClaimName, expectedClaimValue, actualClaimValue + ); + invalidClaimException = new IncorrectClaimException(header, claims, msg); + } + + if (invalidClaimException != null) { + invalidClaimException.setClaimName(expectedClaimName); + invalidClaimException.setClaimValue(expectedClaimValue); + throw invalidClaimException; + } + } + } + /* * @since 0.5 mostly to allow testing overrides */ diff --git a/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy new file mode 100644 index 00000000..3a3062ab --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/IncorrectClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 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 + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class IncorrectClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new IncorrectClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} diff --git a/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy new file mode 100644 index 00000000..5209a041 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/InvalidClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 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 + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class InvalidClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new InvalidClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a831c4b1..40c4986c 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -723,4 +723,129 @@ class JwtParserTest { 'method or, for HMAC algorithms, the resolveSigningKeyBytes(JwsHeader, String) method.' } } + + @Test + void testParseExpectIgnoreNullClaimName() { + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(null, expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectIgnoreEmptyClaimName() { + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect("", expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectIgnoreNullClaimValue() { + def expectedClaimName = 'A Most Awesome Claim Name' + + byte[] key = randomKey() + + // not setting expected claim name in JWT + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + // expecting claim name, but with null value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, null). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), 'Dummy' + } + + @Test + void testParseExpectGeneric_Success() { + def expectedClaimName = 'A Most Awesome Claim Name' + def expectedClaimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim(expectedClaimName, expectedClaimValue). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, expectedClaimValue). + parseClaimsJws(compact) + + assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue + } + + @Test + void testParseExpectGeneric_Incorrect_Fail() { + def goodClaimName = 'A Most Awesome Claim Name' + def goodClaimValue = 'A Most Awesome Claim Value' + + def badClaimValue = 'A Most Bogus Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim(goodClaimName, badClaimValue). + compact() + + try { + Jwts.parser().setSigningKey(key). + expect(goodClaimName, goodClaimValue). + parseClaimsJws(compact) + fail() + } catch (IncorrectClaimException e) { + assertEquals( + String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), + e.getMessage() + ) + } + } + + @Test + void testParseExpectedGeneric_Missing_Fail() { + def claimName = 'A Most Awesome Claim Name' + def claimValue = 'A Most Awesome Claim Value' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('Dummy'). + compact() + + try { + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(claimName, claimValue). + parseClaimsJws(compact) + fail() + } catch (MissingClaimException e) { + assertEquals( + String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + e.getMessage() + ) + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy new file mode 100644 index 00000000..36658df6 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/MissingClaimExceptionTest.groovy @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 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 + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class MissingClaimExceptionTest { + + @Test + void testOverloadedConstructor() { + def header = Jwts.header() + def claims = Jwts.claims() + def msg = 'foo' + def cause = new NullPointerException() + + def claimName = 'cName' + def claimValue = 'cValue' + + def ex = new MissingClaimException(header, claims, msg, cause) + ex.setClaimName(claimName) + ex.setClaimValue(claimValue) + + assertSame ex.header, header + assertSame ex.claims, claims + assertEquals ex.message, msg + assertSame ex.cause, cause + assertEquals ex.claimName, claimName + assertEquals ex.claimValue, claimValue + } +} From 0fab5504cdf6f179b5ab3f450afec7c5aa644eec Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 02:53:57 -0400 Subject: [PATCH 08/50] Added expectIssuedAt convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 9 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 24 +++++- .../io/jsonwebtoken/JwtParserTest.groovy | 73 ++++++++++++++++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index c26da433..fbf2a81f 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -16,6 +16,7 @@ package io.jsonwebtoken; import java.security.Key; +import java.util.Date; /** * A parser for reading JWT strings, used to convert them into a {@link Jwt} object representing the expanded JWT. @@ -26,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the issuedAt claim. + * + * @param issuedAt + * @return the parser for method chaining. + */ + JwtParser expectIssuedAt(Date issuedAt); + /** * Sets an expected value for any given claim name. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 36142292..26f17f0e 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -46,7 +46,6 @@ import java.io.IOException; import java.security.Key; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.LinkedHashMap; import java.util.Map; @SuppressWarnings("unchecked") @@ -63,7 +62,16 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; - Map expectedClaims = new LinkedHashMap(); + Claims expectedClaims = new DefaultClaims(); + + @Override + public JwtParser expectIssuedAt(Date issuedAt) { + if (issuedAt != null) { + expectedClaims.setIssuedAt(issuedAt); + } + + return this; + } @Override public JwtParser expect(String claimName, Object value) { @@ -329,8 +337,16 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = expectedClaims.get(expectedClaimName); - Object actualClaimValue = claims.get(expectedClaimName); + Object expectedClaimValue = null; + Object actualClaimValue = null; + if (Claims.ISSUED_AT.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getIssuedAt(); + actualClaimValue = claims.getIssuedAt(); + } else { + expectedClaimValue = expectedClaims.get(expectedClaimName); + actualClaimValue = claims.get(expectedClaimName); + } + InvalidClaimException invalidClaimException = null; if (actualClaimValue == null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 40c4986c..c5726dab 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -22,6 +22,8 @@ import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom import static org.junit.Assert.* +import static ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE class JwtParserTest { @@ -819,7 +821,7 @@ class JwtParserTest { fail() } catch (IncorrectClaimException e) { assertEquals( - String.format(ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, goodClaimName, goodClaimValue, badClaimValue), e.getMessage() ) } @@ -843,7 +845,74 @@ class JwtParserTest { fail() } catch (MissingClaimException e) { assertEquals( - String.format(ClaimJwtException.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuedAt_Success() { + def issuedAt = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuedAt(issuedAt). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectIssuedAt(issuedAt). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def issuedAtMillis = ((long)issuedAt.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis + } + + @Test + void testParseExpectIssuedAt_Incorrect_Fail() { + def goodIssuedAt = new Date(System.currentTimeMillis()) + def badIssuedAt = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuedAt(badIssuedAt). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuedAt(goodIssuedAt). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, goodIssuedAt, badIssuedAt), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuedAt_Missing_Fail() { + def issuedAt = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuedAt(issuedAt). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUED_AT, issuedAt), e.getMessage() ) } From 056dc819e2cc44deb8925fe534b2a23d20e3db4c Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:08:03 -0400 Subject: [PATCH 09/50] Added expectIssuer convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 15 ++++- .../io/jsonwebtoken/JwtParserTest.groovy | 64 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index fbf2a81f..f9c2e54e 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the issuer claim. + * + * @param issuer + * @return the parser for method chaining. + */ + JwtParser expectIssuer(String issuer); + /** * Sets an expected value for the issuedAt claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 26f17f0e..be898304 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -73,6 +73,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectIssuer(String issuer) { + expect(Claims.ISSUER, issuer); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { @@ -337,8 +344,12 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue = null; - Object actualClaimValue = null; + Object expectedClaimValue; + Object actualClaimValue; + + // since issued at is a date, call the specific method + // other methods deal with strings and the more + // general method can be used if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index c5726dab..da3dbb21 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -917,4 +917,68 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectIssuer_Success() { + def issuer = 'A Most Awesome Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer(issuer). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectIssuer(issuer). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getIssuer(), issuer + } + + @Test + void testParseExpectIssuer_Incorrect_Fail() { + def goodIssuer = 'A Most Awesome Issuer' + def badIssuer = 'A Most Bogus Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer(badIssuer). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuer(goodIssuer). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, goodIssuer, badIssuer), + e.getMessage() + ) + } + } + + @Test + void testParseExpectIssuer_Missing_Fail() { + def issuer = 'A Most Awesome Issuer' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectIssuer(issuer). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, issuer), + e.getMessage() + ) + } + } } From fd04a357cb4e0ae90d991c94924335032bc9dbe5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:32:56 -0400 Subject: [PATCH 10/50] Added expectAudience convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 65 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f9c2e54e..9f6906c7 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the audience claim. + * + * @param audience + * @return the parser for method chaining. + */ + JwtParser expectAudience(String audience); + /** * Sets an expected value for the issuer claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index be898304..08664d09 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -80,6 +80,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectAudience(String audience) { + expect(Claims.AUDIENCE, audience); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index da3dbb21..7cd1d88d 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -981,4 +981,69 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectAudience_Success() { + def audience = 'A Most Awesome Audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setAudience(audience). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectAudience(audience). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getAudience(), audience + } + + @Test + void testParseExpectAudience_Incorrect_Fail() { + def goodAudience = 'A Most Awesome Audience' + def badAudience = 'A Most Bogus Audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setAudience(badAudience). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectAudience(goodAudience). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, goodAudience, badAudience), + e.getMessage() + ) + } + } + + @Test + void testParseExpectAudience_Missing_Fail() { + def audience = 'A Most Awesome audience' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectAudience(audience). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, audience), + e.getMessage() + ) + } + } + } From f3c8f10f32d89a447ead2311536a5de90b5a02eb Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:40:20 -0400 Subject: [PATCH 11/50] Added expectSubject convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 +++ .../io/jsonwebtoken/JwtParserTest.groovy | 63 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 9f6906c7..c74ec9e6 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the subject claim. + * + * @param subject + * @return the parser for method chaining. + */ + JwtParser expectSubject(String subject); + /** * Sets an expected value for the audience claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 08664d09..a251ca8e 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -87,6 +87,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectSubject(String subject) { + expect(Claims.SUBJECT, subject); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 7cd1d88d..c6116842 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1046,4 +1046,67 @@ class JwtParserTest { } } + @Test + void testParseExpectSubject_Success() { + def subject = 'A Most Awesome Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject(subject). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectSubject(subject). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseExpectSubject_Incorrect_Fail() { + def goodSubject = 'A Most Awesome Subject' + def badSubject = 'A Most Bogus Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject(badSubject). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectSubject(goodSubject). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, goodSubject, badSubject), + e.getMessage() + ) + } + } + + @Test + void testParseExpectSubject_Missing_Fail() { + def subject = 'A Most Awesome Subject' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId('id'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectSubject(subject). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, subject), + e.getMessage() + ) + } + } } From f2e620e36bff02804f99a20b5190cafd42bb22b3 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 12 Sep 2015 03:51:00 -0400 Subject: [PATCH 12/50] Added expectId convenience method. --- src/main/java/io/jsonwebtoken/JwtParser.java | 8 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 7 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 64 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index c74ec9e6..3e0cff47 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -27,6 +27,14 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; + /** + * Sets an expected value for the jti claim. + * + * @param id + * @return the parser for method chaining. + */ + JwtParser expectId(String id); + /** * Sets an expected value for the subject claim. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a251ca8e..30588e1f 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -94,6 +94,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser expectId(String id) { + expect(Claims.ID, id); + + return this; + } + @Override public JwtParser expect(String claimName, Object value) { if (claimName != null && claimName.length() > 0 && value != null) { diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index c6116842..a5b74580 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1109,4 +1109,68 @@ class JwtParserTest { ) } } + + @Test + void testParseExpectId_Success() { + def id = 'A Most Awesome id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId(id). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + expectId(id). + parseClaimsJws(compact) + + assertEquals jwt.getBody().getId(), id + } + + @Test + void testParseExpectId_Incorrect_Fail() { + def goodId = 'A Most Awesome Id' + def badId = 'A Most Bogus Id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setId(badId). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectId(goodId). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, goodId, badId), + e.getMessage() + ) + } + } + + @Test + void testParseExpectId_Missing_Fail() { + def id = 'A Most Awesome Id' + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setIssuer('me'). + compact() + + try { + Jwts.parser().setSigningKey(key). + expectId(id). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, id), + e.getMessage() + ) + } + } } From 5ecaacde5a61b8f1c1f7ad711ab51720bb095aa6 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 01:12:54 -0400 Subject: [PATCH 13/50] Don't allow empty or null claimName or null value for claim expectations. --- .../jsonwebtoken/impl/DefaultJwtParser.java | 6 +- .../io/jsonwebtoken/JwtParserTest.groovy | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 30588e1f..9e9c5ee9 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -103,9 +103,9 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser expect(String claimName, Object value) { - if (claimName != null && claimName.length() > 0 && value != null) { - expectedClaims.put(claimName, value); - } + Assert.hasText(claimName, "claim name cannot be null or empty."); + Assert.notNull(value, "The value cannot be null for claim name: " + claimName); + expectedClaims.put(claimName, value); return this; } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a5b74580..ea0bda6a 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -727,7 +727,7 @@ class JwtParserTest { } @Test - void testParseExpectIgnoreNullClaimName() { + void testParseExpectDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -737,35 +737,47 @@ class JwtParserTest { setIssuer('Dummy'). compact() - // expecting null claim name, but with value - Jwt jwt = Jwts.parser().setSigningKey(key). - expect(null, expectedClaimValue). - parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(null, expectedClaimValue). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } } @Test - void testParseExpectIgnoreEmptyClaimName() { + void testParseExpectDontAllowEmptyClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() // not setting expected claim name in JWT String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). - setIssuer('Dummy'). - compact() + setIssuer('Dummy'). + compact() - // expecting null claim name, but with value - Jwt jwt = Jwts.parser().setSigningKey(key). + try { + // expecting null claim name, but with value + Jwt jwt = Jwts.parser().setSigningKey(key). expect("", expectedClaimValue). parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "claim name cannot be null or empty.", + e.getMessage() + ) + } } @Test - void testParseExpectIgnoreNullClaimValue() { + void testParseExpectDontAllowNullClaimValue() { def expectedClaimName = 'A Most Awesome Claim Name' byte[] key = randomKey() @@ -775,12 +787,18 @@ class JwtParserTest { setIssuer('Dummy'). compact() - // expecting claim name, but with null value - Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, null). - parseClaimsJws(compact) - - assertEquals jwt.getBody().getIssuer(), 'Dummy' + try { + // expecting claim name, but with null value + Jwt jwt = Jwts.parser().setSigningKey(key). + expect(expectedClaimName, null). + parseClaimsJws(compact) + fail() + } catch (IllegalArgumentException e) { + assertEquals( + "The value cannot be null for claim name: " + expectedClaimName, + e.getMessage() + ) + } } @Test From 62ccd16748f3c89b41dfdd62b8978c5ade772e29 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 01:31:17 -0400 Subject: [PATCH 14/50] Call underlying delegate methods for expected claims. --- .../java/io/jsonwebtoken/impl/DefaultJwtParser.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 9e9c5ee9..228a40c0 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -66,37 +66,35 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser expectIssuedAt(Date issuedAt) { - if (issuedAt != null) { - expectedClaims.setIssuedAt(issuedAt); - } + expectedClaims.setIssuedAt(issuedAt); return this; } @Override public JwtParser expectIssuer(String issuer) { - expect(Claims.ISSUER, issuer); + expectedClaims.setIssuer(issuer); return this; } @Override public JwtParser expectAudience(String audience) { - expect(Claims.AUDIENCE, audience); + expectedClaims.setAudience(audience); return this; } @Override public JwtParser expectSubject(String subject) { - expect(Claims.SUBJECT, subject); + expectedClaims.setSubject(subject); return this; } @Override public JwtParser expectId(String id) { - expect(Claims.ID, id); + expectedClaims.setId(id); return this; } From 5dd95b67553123a8d8d4c62b9137604078675599 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:18:10 -0400 Subject: [PATCH 15/50] Refactored validateExpectedClaims --- .../jsonwebtoken/impl/DefaultJwtParser.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 228a40c0..5a815c76 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -363,18 +363,32 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - Object expectedClaimValue; - Object actualClaimValue; - // since issued at is a date, call the specific method - // other methods deal with strings and the more - // general method can be used + // this will be overridden if one of the default claims is used + Object expectedClaimValue = expectedClaims.get(expectedClaimName); + Object actualClaimValue = claims.get(expectedClaimName); + if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); - } else { - expectedClaimValue = expectedClaims.get(expectedClaimName); - actualClaimValue = claims.get(expectedClaimName); + } else if (Claims.AUDIENCE.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getAudience(); + actualClaimValue = claims.getAudience(); + } else if (Claims.ISSUER.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getIssuer(); + actualClaimValue = claims.getIssuer(); + } else if (Claims.SUBJECT.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getSubject(); + actualClaimValue = claims.getSubject(); + } else if (Claims.EXPIRATION.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getExpiration(); + actualClaimValue = claims.getExpiration(); + } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getNotBefore(); + actualClaimValue = claims.getNotBefore(); + } else if (Claims.ID.equals(expectedClaimName)) { + expectedClaimValue = expectedClaims.getId(); + actualClaimValue = claims.getId(); } InvalidClaimException invalidClaimException = null; @@ -385,8 +399,7 @@ public class DefaultJwtParser implements JwtParser { expectedClaimName, expectedClaimValue ); invalidClaimException = new MissingClaimException(header, claims, msg); - } - else if (!expectedClaimValue.equals(actualClaimValue)) { + } else if (!expectedClaimValue.equals(actualClaimValue)) { String msg = String.format( ClaimJwtException.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, expectedClaimName, expectedClaimValue, actualClaimValue From 72acd649c7092d3d2d2d23c4cb3306bf8a9bd902 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:25:29 -0400 Subject: [PATCH 16/50] Updated custom Exception javadocs. --- src/main/java/io/jsonwebtoken/IncorrectClaimException.java | 4 ++-- src/main/java/io/jsonwebtoken/InvalidClaimException.java | 6 +++++- src/main/java/io/jsonwebtoken/MissingClaimException.java | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java index bf63cd5a..860a4d84 100644 --- a/src/main/java/io/jsonwebtoken/IncorrectClaimException.java +++ b/src/main/java/io/jsonwebtoken/IncorrectClaimException.java @@ -16,8 +16,8 @@ package io.jsonwebtoken; /** - * IncorrectClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an - * expected claim has a value that is not expected. + * Exception thrown when discovering that a required claim does not equal the required value, indicating the JWT is + * invalid and may not be used. * * @since 0.6 */ diff --git a/src/main/java/io/jsonwebtoken/InvalidClaimException.java b/src/main/java/io/jsonwebtoken/InvalidClaimException.java index 9706a0b3..8880792c 100644 --- a/src/main/java/io/jsonwebtoken/InvalidClaimException.java +++ b/src/main/java/io/jsonwebtoken/InvalidClaimException.java @@ -16,7 +16,11 @@ package io.jsonwebtoken; /** - * InvalidClaimException is a subclass of the {@link ClaimJwtException} that is thrown after a validation of an JTW claim failed. + * Exception indicating a parsed claim is invalid in some way. Subclasses reflect the specific + * reason the claim is invalid. + * + * @see IncorrectClaimException + * @see MissingClaimException * * @since 0.6 */ diff --git a/src/main/java/io/jsonwebtoken/MissingClaimException.java b/src/main/java/io/jsonwebtoken/MissingClaimException.java index d453df57..030fe98d 100644 --- a/src/main/java/io/jsonwebtoken/MissingClaimException.java +++ b/src/main/java/io/jsonwebtoken/MissingClaimException.java @@ -16,8 +16,8 @@ package io.jsonwebtoken; /** - * MissingClaimException is a subclass of the {@link InvalidClaimException} that is thrown after it is found that an - * expected claim is missing. + * Exception thrown when discovering that a required claim is not present, indicating the JWT is + * invalid and may not be used. * * @since 0.6 */ From ddda2f92d3d5e36c1b5469eea553d89e7b6e6de0 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:35:41 -0400 Subject: [PATCH 17/50] Renamed all the expect methods to require. --- src/main/java/io/jsonwebtoken/JwtParser.java | 12 +-- .../jsonwebtoken/impl/DefaultJwtParser.java | 12 +-- .../io/jsonwebtoken/JwtParserTest.groovy | 84 +++++++++---------- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 3e0cff47..1ed015ff 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -33,7 +33,7 @@ public interface JwtParser { * @param id * @return the parser for method chaining. */ - JwtParser expectId(String id); + JwtParser requireId(String id); /** * Sets an expected value for the subject claim. @@ -41,7 +41,7 @@ public interface JwtParser { * @param subject * @return the parser for method chaining. */ - JwtParser expectSubject(String subject); + JwtParser requireSubject(String subject); /** * Sets an expected value for the audience claim. @@ -49,7 +49,7 @@ public interface JwtParser { * @param audience * @return the parser for method chaining. */ - JwtParser expectAudience(String audience); + JwtParser requireAudience(String audience); /** * Sets an expected value for the issuer claim. @@ -57,7 +57,7 @@ public interface JwtParser { * @param issuer * @return the parser for method chaining. */ - JwtParser expectIssuer(String issuer); + JwtParser requireIssuer(String issuer); /** * Sets an expected value for the issuedAt claim. @@ -65,7 +65,7 @@ public interface JwtParser { * @param issuedAt * @return the parser for method chaining. */ - JwtParser expectIssuedAt(Date issuedAt); + JwtParser requireIssuedAt(Date issuedAt); /** * Sets an expected value for any given claim name. @@ -82,7 +82,7 @@ public interface JwtParser { * @param value * @return the parser for method chaining. */ - JwtParser expect(String claimName, Object value); + JwtParser require(String claimName, Object value); /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 5a815c76..a52e7b54 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -65,42 +65,42 @@ public class DefaultJwtParser implements JwtParser { Claims expectedClaims = new DefaultClaims(); @Override - public JwtParser expectIssuedAt(Date issuedAt) { + public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); return this; } @Override - public JwtParser expectIssuer(String issuer) { + public JwtParser requireIssuer(String issuer) { expectedClaims.setIssuer(issuer); return this; } @Override - public JwtParser expectAudience(String audience) { + public JwtParser requireAudience(String audience) { expectedClaims.setAudience(audience); return this; } @Override - public JwtParser expectSubject(String subject) { + public JwtParser requireSubject(String subject) { expectedClaims.setSubject(subject); return this; } @Override - public JwtParser expectId(String id) { + public JwtParser requireId(String id) { expectedClaims.setId(id); return this; } @Override - public JwtParser expect(String claimName, Object value) { + public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); Assert.notNull(value, "The value cannot be null for claim name: " + claimName); expectedClaims.put(claimName, value); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index ea0bda6a..4fbd36ca 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -727,7 +727,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowNullClaimName() { + void testParseRequireDontAllowNullClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -740,7 +740,7 @@ class JwtParserTest { try { // expecting null claim name, but with value Jwt jwt = Jwts.parser().setSigningKey(key). - expect(null, expectedClaimValue). + require(null, expectedClaimValue). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -752,7 +752,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowEmptyClaimName() { + void testParseRequireDontAllowEmptyClaimName() { def expectedClaimValue = 'A Most Awesome Claim Value' byte[] key = randomKey() @@ -765,7 +765,7 @@ class JwtParserTest { try { // expecting null claim name, but with value Jwt jwt = Jwts.parser().setSigningKey(key). - expect("", expectedClaimValue). + require("", expectedClaimValue). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -777,7 +777,7 @@ class JwtParserTest { } @Test - void testParseExpectDontAllowNullClaimValue() { + void testParseRequireDontAllowNullClaimValue() { def expectedClaimName = 'A Most Awesome Claim Name' byte[] key = randomKey() @@ -790,7 +790,7 @@ class JwtParserTest { try { // expecting claim name, but with null value Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, null). + require(expectedClaimName, null). parseClaimsJws(compact) fail() } catch (IllegalArgumentException e) { @@ -802,7 +802,7 @@ class JwtParserTest { } @Test - void testParseExpectGeneric_Success() { + void testParseRequireGeneric_Success() { def expectedClaimName = 'A Most Awesome Claim Name' def expectedClaimValue = 'A Most Awesome Claim Value' @@ -813,14 +813,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expect(expectedClaimName, expectedClaimValue). + require(expectedClaimName, expectedClaimValue). parseClaimsJws(compact) assertEquals jwt.getBody().get(expectedClaimName), expectedClaimValue } @Test - void testParseExpectGeneric_Incorrect_Fail() { + void testParseRequireGeneric_Incorrect_Fail() { def goodClaimName = 'A Most Awesome Claim Name' def goodClaimValue = 'A Most Awesome Claim Value' @@ -834,7 +834,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expect(goodClaimName, goodClaimValue). + require(goodClaimName, goodClaimValue). parseClaimsJws(compact) fail() } catch (IncorrectClaimException e) { @@ -846,7 +846,7 @@ class JwtParserTest { } @Test - void testParseExpectedGeneric_Missing_Fail() { + void testParseRequireedGeneric_Missing_Fail() { def claimName = 'A Most Awesome Claim Name' def claimValue = 'A Most Awesome Claim Value' @@ -858,7 +858,7 @@ class JwtParserTest { try { Jwt jwt = Jwts.parser().setSigningKey(key). - expect(claimName, claimValue). + require(claimName, claimValue). parseClaimsJws(compact) fail() } catch (MissingClaimException e) { @@ -870,7 +870,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Success() { + void testParseRequireIssuedAt_Success() { def issuedAt = new Date(System.currentTimeMillis()) byte[] key = randomKey() @@ -880,7 +880,7 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectIssuedAt(issuedAt). + requireIssuedAt(issuedAt). parseClaimsJws(compact) // system converts to seconds (lopping off millis precision), then returns millis @@ -890,7 +890,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Incorrect_Fail() { + void testParseRequireIssuedAt_Incorrect_Fail() { def goodIssuedAt = new Date(System.currentTimeMillis()) def badIssuedAt = new Date(System.currentTimeMillis() - 10000) @@ -902,7 +902,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuedAt(goodIssuedAt). + requireIssuedAt(goodIssuedAt). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -914,7 +914,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuedAt_Missing_Fail() { + void testParseRequireIssuedAt_Missing_Fail() { def issuedAt = new Date(System.currentTimeMillis() - 10000) byte[] key = randomKey() @@ -925,7 +925,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuedAt(issuedAt). + requireIssuedAt(issuedAt). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -937,7 +937,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuer_Success() { + void testParseRequireIssuer_Success() { def issuer = 'A Most Awesome Issuer' byte[] key = randomKey() @@ -947,14 +947,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectIssuer(issuer). + requireIssuer(issuer). parseClaimsJws(compact) assertEquals jwt.getBody().getIssuer(), issuer } @Test - void testParseExpectIssuer_Incorrect_Fail() { + void testParseRequireIssuer_Incorrect_Fail() { def goodIssuer = 'A Most Awesome Issuer' def badIssuer = 'A Most Bogus Issuer' @@ -966,7 +966,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuer(goodIssuer). + requireIssuer(goodIssuer). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -978,7 +978,7 @@ class JwtParserTest { } @Test - void testParseExpectIssuer_Missing_Fail() { + void testParseRequireIssuer_Missing_Fail() { def issuer = 'A Most Awesome Issuer' byte[] key = randomKey() @@ -989,7 +989,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectIssuer(issuer). + requireIssuer(issuer). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1001,7 +1001,7 @@ class JwtParserTest { } @Test - void testParseExpectAudience_Success() { + void testParseRequireAudience_Success() { def audience = 'A Most Awesome Audience' byte[] key = randomKey() @@ -1011,14 +1011,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectAudience(audience). + requireAudience(audience). parseClaimsJws(compact) assertEquals jwt.getBody().getAudience(), audience } @Test - void testParseExpectAudience_Incorrect_Fail() { + void testParseRequireAudience_Incorrect_Fail() { def goodAudience = 'A Most Awesome Audience' def badAudience = 'A Most Bogus Audience' @@ -1030,7 +1030,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectAudience(goodAudience). + requireAudience(goodAudience). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1042,7 +1042,7 @@ class JwtParserTest { } @Test - void testParseExpectAudience_Missing_Fail() { + void testParseRequireAudience_Missing_Fail() { def audience = 'A Most Awesome audience' byte[] key = randomKey() @@ -1053,7 +1053,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectAudience(audience). + requireAudience(audience). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1065,7 +1065,7 @@ class JwtParserTest { } @Test - void testParseExpectSubject_Success() { + void testParseRequireSubject_Success() { def subject = 'A Most Awesome Subject' byte[] key = randomKey() @@ -1075,14 +1075,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectSubject(subject). + requireSubject(subject). parseClaimsJws(compact) assertEquals jwt.getBody().getSubject(), subject } @Test - void testParseExpectSubject_Incorrect_Fail() { + void testParseRequireSubject_Incorrect_Fail() { def goodSubject = 'A Most Awesome Subject' def badSubject = 'A Most Bogus Subject' @@ -1094,7 +1094,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectSubject(goodSubject). + requireSubject(goodSubject). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1106,7 +1106,7 @@ class JwtParserTest { } @Test - void testParseExpectSubject_Missing_Fail() { + void testParseRequireSubject_Missing_Fail() { def subject = 'A Most Awesome Subject' byte[] key = randomKey() @@ -1117,7 +1117,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectSubject(subject). + requireSubject(subject). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { @@ -1129,7 +1129,7 @@ class JwtParserTest { } @Test - void testParseExpectId_Success() { + void testParseRequireId_Success() { def id = 'A Most Awesome id' byte[] key = randomKey() @@ -1139,14 +1139,14 @@ class JwtParserTest { compact() Jwt jwt = Jwts.parser().setSigningKey(key). - expectId(id). + requireId(id). parseClaimsJws(compact) assertEquals jwt.getBody().getId(), id } @Test - void testParseExpectId_Incorrect_Fail() { + void testParseRequireId_Incorrect_Fail() { def goodId = 'A Most Awesome Id' def badId = 'A Most Bogus Id' @@ -1158,7 +1158,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectId(goodId). + requireId(goodId). parseClaimsJws(compact) fail() } catch(IncorrectClaimException e) { @@ -1170,7 +1170,7 @@ class JwtParserTest { } @Test - void testParseExpectId_Missing_Fail() { + void testParseRequireId_Missing_Fail() { def id = 'A Most Awesome Id' byte[] key = randomKey() @@ -1181,7 +1181,7 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - expectId(id). + requireId(id). parseClaimsJws(compact) fail() } catch(MissingClaimException e) { From 2e452a42b10173ee0708eedacb845ba0b315c5d0 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 04:33:31 -0400 Subject: [PATCH 18/50] Added requireExpiration and requireNotBefore --- src/main/java/io/jsonwebtoken/JwtParser.java | 16 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 14 ++ .../io/jsonwebtoken/JwtParserTest.groovy | 136 ++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 1ed015ff..828563ea 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -67,6 +67,22 @@ public interface JwtParser { */ JwtParser requireIssuedAt(Date issuedAt); + /** + * Sets an expected value for the expiration claim. + * + * @param expiration + * @return the parser for method chaining. + */ + JwtParser requireExpiration(Date expiration); + + /** + * Sets an expected value for the notBefore claim. + * + * @param notBefore + * @return the parser for method chaining + */ + JwtParser requireNotBefore(Date notBefore); + /** * Sets an expected value for any given claim name. * diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a52e7b54..37c5f5a0 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -99,6 +99,20 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser requireExpiration(Date expiration) { + expectedClaims.setExpiration(expiration); + + return this; + } + + @Override + public JwtParser requireNotBefore(Date notBefore) { + expectedClaims.setNotBefore(notBefore); + + return this; + } + @Override public JwtParser require(String claimName, Object value) { Assert.hasText(claimName, "claim name cannot be null or empty."); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 4fbd36ca..a65aa4dc 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1191,4 +1191,140 @@ class JwtParserTest { ) } } + + @Test + void testParseRequireExpiration_Success() { + // expire in the future + def expiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setExpiration(expiration). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + requireExpiration(expiration). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def expirationMillis = ((long)expiration.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis + } + + @Test + void testParseRequireExpirationAt_Incorrect_Fail() { + def goodExpiration = new Date(System.currentTimeMillis() + 20000) + def badExpiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setExpiration(badExpiration). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireExpiration(goodExpiration). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, goodExpiration, badExpiration), + e.getMessage() + ) + } + } + + @Test + void testParseRequireExpiration_Missing_Fail() { + def expiration = new Date(System.currentTimeMillis() + 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireExpiration(expiration). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.EXPIRATION, expiration), + e.getMessage() + ) + } + } + + @Test + void testParseRequireNotBefore_Success() { + // expire in the future + def notBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setNotBefore(notBefore). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + requireNotBefore(notBefore). + parseClaimsJws(compact) + + // system converts to seconds (lopping off millis precision), then returns millis + def notBeforeMillis = ((long)notBefore.getTime() / 1000) * 1000 + + assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis + } + + @Test + void testParseRequireNotBefore_Incorrect_Fail() { + def goodNotBefore = new Date(System.currentTimeMillis() - 20000) + def badNotBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setNotBefore(badNotBefore). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireNotBefore(goodNotBefore). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, goodNotBefore, badNotBefore), + e.getMessage() + ) + } + } + + @Test + void testParseRequireNotBefore_Missing_Fail() { + def notBefore = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + requireNotBefore(notBefore). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.NOT_BEFORE, notBefore), + e.getMessage() + ) + } + } } From b4015be11e4337977f743fe0d78d949c14ec3ae8 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 03:37:47 -0400 Subject: [PATCH 19/50] Added in test that does NOT work for custom claim with Date type. --- .../groovy/io/jsonwebtoken/JwtParserTest.groovy | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index a65aa4dc..60299845 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1327,4 +1327,21 @@ class JwtParserTest { ) } } + +// @Test +// void testParseExpectedCustomDate_Success() { +// def aDate = new Date(System.currentTimeMillis()) +// +// byte[] key = randomKey() +// +// String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). +// claim("aDate", aDate). +// compact() +// +// Jwt jwt = Jwts.parser().setSigningKey(key). +// expect("aDate", aDate). +// parseClaimsJws(compact) +// +// assertEquals jwt.getBody().get("aDate"), aDate +// } } From 5d320d22a55c4822195852d7fded90101a728eca Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 15:43:15 -0400 Subject: [PATCH 20/50] Handled generic require for Date. Added ability to specify required type on get method of claim --- src/main/java/io/jsonwebtoken/Claims.java | 1 + .../jsonwebtoken/RequiredTypeException.java | 32 ++++++++ .../io/jsonwebtoken/impl/DefaultClaims.java | 17 ++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 15 +--- .../io/jsonwebtoken/JwtParserTest.groovy | 80 +++++++++++++++---- .../RequiredTypeExceptionTest.groovy | 20 +++++ .../impl/DefaultClaimsTest.groovy | 78 ++++++++++++++++++ 7 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/RequiredTypeException.java create mode 100644 src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy diff --git a/src/main/java/io/jsonwebtoken/Claims.java b/src/main/java/io/jsonwebtoken/Claims.java index d510aca8..22a3e364 100644 --- a/src/main/java/io/jsonwebtoken/Claims.java +++ b/src/main/java/io/jsonwebtoken/Claims.java @@ -170,4 +170,5 @@ public interface Claims extends Map, ClaimsMutator { @Override //only for better/targeted JavaDoc Claims setId(String jti); + T get(String claimName, Class requiredType); } diff --git a/src/main/java/io/jsonwebtoken/RequiredTypeException.java b/src/main/java/io/jsonwebtoken/RequiredTypeException.java new file mode 100644 index 00000000..eeb60d30 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/RequiredTypeException.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Exception thrown when {@link Claims#get(String, Class)} is called and the value does not match the type of the + * {@code Class} argument. + * + * @since 0.6 + */ +public class RequiredTypeException extends JwtException { + public RequiredTypeException(String message) { + super(message); + } + + public RequiredTypeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 5e6284eb..38df87ee 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.RequiredTypeException; import java.util.Date; import java.util.Map; @@ -106,4 +107,20 @@ public class DefaultClaims extends JwtMap implements Claims { setValue(Claims.ID, jti); return this; } + + @Override + public T get(String claimName, Class requiredType) { + Object value = get(claimName); + if (value == null) { return null; } + + if (requiredType == Date.class && value instanceof Long) { + value = new Date((Long)value); + } + + if (!requiredType.isInstance(value)) { + throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); + } + + return requiredType.cast(value); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 37c5f5a0..9bbe3b2b 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -378,31 +378,20 @@ public class DefaultJwtParser implements JwtParser { private void validateExpectedClaims(Header header, Claims claims) { for (String expectedClaimName : expectedClaims.keySet()) { - // this will be overridden if one of the default claims is used Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); if (Claims.ISSUED_AT.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getIssuedAt(); actualClaimValue = claims.getIssuedAt(); - } else if (Claims.AUDIENCE.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getAudience(); - actualClaimValue = claims.getAudience(); - } else if (Claims.ISSUER.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getIssuer(); - actualClaimValue = claims.getIssuer(); - } else if (Claims.SUBJECT.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getSubject(); - actualClaimValue = claims.getSubject(); } else if (Claims.EXPIRATION.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getExpiration(); actualClaimValue = claims.getExpiration(); } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { expectedClaimValue = expectedClaims.getNotBefore(); actualClaimValue = claims.getNotBefore(); - } else if (Claims.ID.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getId(); - actualClaimValue = claims.getId(); + } else if (expectedClaimValue instanceof Date && actualClaimValue != null && actualClaimValue instanceof Long) { + actualClaimValue = new Date((Long)actualClaimValue); } InvalidClaimException invalidClaimException = null; diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 60299845..3e37658f 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1328,20 +1328,68 @@ class JwtParserTest { } } -// @Test -// void testParseExpectedCustomDate_Success() { -// def aDate = new Date(System.currentTimeMillis()) -// -// byte[] key = randomKey() -// -// String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). -// claim("aDate", aDate). -// compact() -// -// Jwt jwt = Jwts.parser().setSigningKey(key). -// expect("aDate", aDate). -// parseClaimsJws(compact) -// -// assertEquals jwt.getBody().get("aDate"), aDate -// } + @Test + void testParseRequireCustomDate_Success() { + def aDate = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("aDate", aDate). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key). + require("aDate", aDate). + parseClaimsJws(compact) + + assertEquals jwt.getBody().get("aDate", Date.class), aDate + } + + @Test + void testParseRequireCustomDate_Incorrect_Fail() { + def goodDate = new Date(System.currentTimeMillis()) + def badDate = new Date(System.currentTimeMillis() - 10000) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("aDate", badDate). + compact() + + try { + Jwts.parser().setSigningKey(key). + require("aDate", goodDate). + parseClaimsJws(compact) + fail() + } catch(IncorrectClaimException e) { + assertEquals( + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", goodDate, badDate), + e.getMessage() + ) + } + + } + + @Test + void testParseRequireCustomDate_Missing_Fail() { + def aDate = new Date(System.currentTimeMillis()) + + byte[] key = randomKey() + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + setSubject("Dummy"). + compact() + + try { + Jwts.parser().setSigningKey(key). + require("aDate", aDate). + parseClaimsJws(compact) + fail() + } catch(MissingClaimException e) { + assertEquals( + String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", aDate), + e.getMessage() + ) + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy new file mode 100644 index 00000000..dded945d --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/RequiredTypeExceptionTest.groovy @@ -0,0 +1,20 @@ +package io.jsonwebtoken + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame + +class RequiredTypeExceptionTest { + @Test + void testOverloadedConstructor() { + def msg = 'foo' + def cause = new NullPointerException() + + def ex = new RequiredTypeException(msg, cause) + + assertEquals ex.message, msg + assertSame ex.cause, cause + } + +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy new file mode 100644 index 00000000..cb584a4d --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 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.impl + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.RequiredTypeException +import org.junit.Before +import org.junit.Test +import static org.junit.Assert.* + +class DefaultClaimsTest { + + Claims claims + + @Before + void setup() { + claims = new DefaultClaims() + } + + @Test + void testGetClaimWithRequiredType_Null_Success() { + claims.put("aNull", null) + Object result = claims.get("aNull", Integer.class) + assertNull(result) + } + + @Test + void testGetClaimWithRequiredType_Exception() { + claims.put("anInteger", new Integer(5)) + try { + claims.get("anInteger", String.class) + fail() + } catch (RequiredTypeException e) { + assertEquals( + "Expected value to be of type: class java.lang.String, but was class java.lang.Integer", + e.getMessage() + ) + } + } + + @Test + void testGetClaimWithRequiredType_Success() { + claims.put("anInteger", new Integer(5)) + Object result = claims.get("anInteger", Integer.class) + + assertTrue(result instanceof Integer) + } + + @Test + void testGetClaimWithRequiredType_Date_Success() { + def actual = new Date(); + claims.put("aDate", actual) + Date expected = claims.get("aDate", Date.class); + assertEquals(expected, actual); + } + + @Test + void testGetClaimWithRequiredType_DateWithLong_Success() { + def actual = new Date(); + // note that Long is stored in claim + claims.put("aDate", actual.getTime()) + Date expected = claims.get("aDate", Date.class); + assertEquals(expected, actual); + } +} From 19f6fcaa51d9f64cfb20c136b002f7ef51c2b0a4 Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Wed, 23 Sep 2015 13:21:08 -0700 Subject: [PATCH 21/50] Issue-52 Adding ability to compress/decompress. Added tests for happy path. --- .gitignore | 1 + .../io/jsonwebtoken/CompressionCodec.java | 30 +++++++ .../CompressionCodecResolver.java | 27 ++++++ .../io/jsonwebtoken/CompressionException.java | 33 +++++++ src/main/java/io/jsonwebtoken/Header.java | 7 ++ src/main/java/io/jsonwebtoken/JwtBuilder.java | 9 ++ src/main/java/io/jsonwebtoken/JwtParser.java | 8 ++ .../io/jsonwebtoken/impl/DefaultHeader.java | 12 +++ .../jsonwebtoken/impl/DefaultJwtBuilder.java | 46 ++++++++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 26 +++++- .../impl/compression/CompressionCodecs.java | 34 +++++++ .../DefaultCompressionCodecResolver.java | 52 +++++++++++ .../compression/DeflateCompressionCodec.java | 84 ++++++++++++++++++ .../compression/GzipCompressionCodec.java | 88 +++++++++++++++++++ .../java/io/jsonwebtoken/lang/Objects.java | 17 ++++ .../java/io/jsonwebtoken/lang/Strings.java | 3 + .../CompressionExceptionTest.groovy | 41 +++++++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 72 +++++++++++++++ .../impl/DefaultJwtBuilderTest.groovy | 2 +- 19 files changed, 581 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/CompressionCodec.java create mode 100644 src/main/java/io/jsonwebtoken/CompressionCodecResolver.java create mode 100644 src/main/java/io/jsonwebtoken/CompressionException.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java create mode 100644 src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy diff --git a/.gitignore b/.gitignore index a14e67cf..611af4c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.class +.DS_Store # Mobile Tools for Java (J2ME) .mtj.tmp/ diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java new file mode 100644 index 00000000..e6a24481 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2015 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; + +/** + * CompressionCodec + * + * @since 0.59 + */ +public interface CompressionCodec { + + String getAlgorithmName(); + + byte[] compress(byte[] payload); + + byte[] decompress(byte[] compressed); +} \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java new file mode 100644 index 00000000..76805e06 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 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; + +/** + * CompressionCodecResolver + * + * @since 0.5.2 + */ +public interface CompressionCodecResolver { + + CompressionCodec resolveCompressionCodec(Header header); + +} diff --git a/src/main/java/io/jsonwebtoken/CompressionException.java b/src/main/java/io/jsonwebtoken/CompressionException.java new file mode 100644 index 00000000..d4ac62d3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/CompressionException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 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; + +/** + * Exception indicating that either compressing or decompressing an JWT body failed. + * + * @since 0.5.2 + */ +public class CompressionException extends JwtException { + + public CompressionException(String message) { + super(message); + } + + public CompressionException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index 00893a58..f4b5cfbf 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -48,6 +48,9 @@ public interface Header> extends Map { /** JWT {@code Content Type} header parameter name: "cty" */ public static final String CONTENT_TYPE = "cty"; + /** JWT {@code Compression Algorithm} header parameter name: "calg" */ + public static final String COMPRESSION_ALGORITHM = "calg"; + /** * Returns the * typ (type) header value or {@code null} if not present. @@ -100,4 +103,8 @@ public interface Header> extends Map { */ T setContentType(String cty); + String getCompressionAlgorithm(); + + T setCompressionAlgorithm(String compressionAlgorithm); + } diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java index cb5339b5..10ae9682 100644 --- a/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -349,6 +349,15 @@ public interface JwtBuilder extends ClaimsMutator { */ JwtBuilder signWith(SignatureAlgorithm alg, Key key); + /** + * Compresses the JWT body before being signed. + * + * @param codec implementation of the {@link CompressionCodec} to be used. + * @return the builder for method chaining. + * @since 0.5.2 + */ + JwtBuilder compressWith(CompressionCodec codec); + /** * Actually builds the JWT and serializes it to a compact, URL-safe string according to the * JWT Compact Serialization diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index f4b190ba..909e73d0 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -107,6 +107,14 @@ public interface JwtParser { */ JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); + /** + * + * @param compressionCodecResolver + * @return the parser for method chaining. + * @since 0.5.2 + */ + JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); + /** * Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false} * otherwise. diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java index 52ffc8b9..74efdf9d 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -51,4 +51,16 @@ public class DefaultHeader> extends JwtMap implements Header setValue(CONTENT_TYPE, cty); return (T)this; } + + @Override + public String getCompressionAlgorithm() { + return getString(COMPRESSION_ALGORITHM); + } + + @Override + public T setCompressionAlgorithm(String compressionAlgorithm) { + setValue(COMPRESSION_ALGORITHM, compressionAlgorithm); + return (T) this; + } + } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 6509ab00..76d3a2c3 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -18,6 +18,7 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.CompressionCodec; import io.jsonwebtoken.Header; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; @@ -48,6 +49,8 @@ public class DefaultJwtBuilder implements JwtBuilder { private Key key; private byte[] keyBytes; + private CompressionCodec compressionCodec; + @Override public JwtBuilder setHeader(Header header) { this.header = header; @@ -113,6 +116,13 @@ public class DefaultJwtBuilder implements JwtBuilder { return this; } + @Override + public JwtBuilder compressWith(CompressionCodec compressionCodec) { + Assert.notNull(compressionCodec, "compressionCodec cannot be null"); + this.compressionCodec = compressionCodec; + return this; + } + @Override public JwtBuilder setPayload(String payload) { this.payload = payload; @@ -279,11 +289,30 @@ public class DefaultJwtBuilder implements JwtBuilder { jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue()); } + if (compressionCodec != null) { + jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName()); + } + String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json."); - String base64UrlEncodedBody = this.payload != null ? - TextCodec.BASE64URL.encode(this.payload) : - base64UrlEncode(claims, "Unable to serialize claims object to json."); + String base64UrlEncodedBody; + + if (compressionCodec != null) { + + byte[] bytes; + try { + bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Unable to serialize claims object to json."); + } + + base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes)); + + } else { + base64UrlEncodedBody = this.payload != null ? + TextCodec.BASE64URL.encode(this.payload) : + base64UrlEncode(claims, "Unable to serialize claims object to json."); + } String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; @@ -311,17 +340,18 @@ public class DefaultJwtBuilder implements JwtBuilder { } protected String base64UrlEncode(Object o, String errMsg) { - String s; + byte[] bytes; try { - s = toJson(o); + bytes = toJson(o); } catch (JsonProcessingException e) { throw new IllegalStateException(errMsg, e); } - return TextCodec.BASE64URL.encode(s); + return TextCodec.BASE64URL.encode(bytes); } - protected String toJson(Object o) throws JsonProcessingException { - return OBJECT_MAPPER.writeValueAsString(o); + + protected byte[] toJson(Object object) throws JsonProcessingException { + return OBJECT_MAPPER.writeValueAsBytes(object); } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ba888e04..a3b372b2 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -17,11 +17,12 @@ package io.jsonwebtoken.impl; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; import io.jsonwebtoken.ExpiredJwtException; 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; @@ -30,7 +31,9 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.PrematureJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; import io.jsonwebtoken.lang.Assert; @@ -58,6 +61,8 @@ public class DefaultJwtParser implements JwtParser { private SigningKeyResolver signingKeyResolver; + private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -86,6 +91,13 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver) { + Assert.notNull(compressionCodecResolver, "compressionCodecResolver cannot be null."); + this.compressionCodecResolver = compressionCodecResolver; + return this; + } + @Override public boolean isSigned(String jwt) { @@ -157,6 +169,8 @@ public class DefaultJwtParser implements JwtParser { // =============== Header ================= Header header = null; + CompressionCodec compressionCodec = null; + if (base64UrlEncodedHeader != null) { String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); Map m = readValue(origValue); @@ -166,10 +180,18 @@ public class DefaultJwtParser implements JwtParser { } else { header = new DefaultHeader(m); } + + compressionCodec = compressionCodecResolver.resolveCompressionCodec(header); } // =============== Body ================= - String payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + String payload; + if (compressionCodec != null) { + byte[] decompressed = compressionCodec.decompress(TextCodec.BASE64URL.decode(base64UrlEncodedPayload)); + payload = new String(decompressed, Strings.UTF_8); + } else { + payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + } Claims claims = null; diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java new file mode 100644 index 00000000..d1d663d3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 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.impl.compression; + +import io.jsonwebtoken.CompressionCodec; + +/** + * CompressionCodecs exposes default implementation of the {@link CompressionCodec} interface. + * + * @since 0.5.2 + */ +public abstract class CompressionCodecs { + + private CompressionCodecs(){} + + public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); + + public static final CompressionCodec GZIP = new GzipCompressionCodec(); + + +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java new file mode 100644 index 00000000..00b37a88 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 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.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionCodecResolver; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Strings; + +/** + * DefaultCompressionCodecResolver + * + * @since 0.5.2 + */ +public class DefaultCompressionCodecResolver implements CompressionCodecResolver { + + @Override + public CompressionCodec resolveCompressionCodec(Header header) { + Assert.notNull(header, "header cannot be null."); + + String cmpAlg = header.getCompressionAlgorithm(); + + if (!Strings.hasText(cmpAlg)) { + return null; + } + + if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { + return CompressionCodecs.DEFLATE; + } + + if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { + return CompressionCodecs.GZIP; + } + + throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java new file mode 100644 index 00000000..83b5725d --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 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.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterOutputStream; + +/** + * DeflateCompressionCodec + * + * @since 0.5.2 + */ +public class DeflateCompressionCodec implements CompressionCodec { + + private static final String DEFLATE = "DEF"; + + @Override + public String getAlgorithmName() { + return DEFLATE; + } + + @Override + public byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); + + ByteArrayOutputStream outputStream = null; + DeflaterOutputStream deflaterOutputStream = null; + try { + outputStream = new ByteArrayOutputStream(); + deflaterOutputStream = new DeflaterOutputStream(outputStream, deflater, true); + + deflaterOutputStream.write(payload, 0, payload.length); + deflaterOutputStream.flush(); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } finally { + Objects.nullSafeClose(outputStream, deflaterOutputStream); + } + } + + @Override + public byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed cannot be null."); + + InflaterOutputStream inflaterOutputStream = null; + ByteArrayOutputStream decompressedOutputStream = null; + + try { + decompressedOutputStream = new ByteArrayOutputStream(); + inflaterOutputStream = new InflaterOutputStream(decompressedOutputStream); + inflaterOutputStream.write(compressed); + inflaterOutputStream.flush(); + return decompressedOutputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to decompress compressed payload.", e); + } finally { + Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java new file mode 100644 index 00000000..1a8a7a53 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 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.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +/** + * GzipCompressionCodec + * + * @since 0.5.2 + */ +public class GzipCompressionCodec implements CompressionCodec { + + private static final String GZIP = "GZIP"; + + @Override + public String getAlgorithmName() { + return GZIP; + } + + @Override + public byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + ByteArrayOutputStream outputStream = null; + GZIPOutputStream gzipOutputStream = null; + + try { + outputStream = new ByteArrayOutputStream(); + gzipOutputStream = new GZIPOutputStream(outputStream, true); + gzipOutputStream.write(payload, 0, payload.length); + gzipOutputStream.finish(); + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } finally { + Objects.nullSafeClose(outputStream, gzipOutputStream); + } + } + + @Override + public byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed cannot be null."); + + byte[] buffer = new byte[512]; + + ByteArrayOutputStream outputStream = null; + GZIPInputStream gzipInputStream = null; + ByteArrayInputStream inputStream = null; + + try { + inputStream = new ByteArrayInputStream(compressed); + gzipInputStream = new GZIPInputStream(inputStream); + outputStream = new ByteArrayOutputStream(); + int read; + while ((read = gzipInputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, read); + } + return outputStream.toByteArray(); + } catch (IOException e) { + throw new CompressionException("Unable to decompress compressed payload.", e); + } finally { + Objects.nullSafeClose(inputStream, gzipInputStream, outputStream); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java index dbdc84ce..acc4b288 100644 --- a/src/main/java/io/jsonwebtoken/lang/Objects.java +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -15,6 +15,8 @@ */ package io.jsonwebtoken.lang; +import java.io.Closeable; +import java.io.IOException; import java.lang.reflect.Array; import java.util.Arrays; @@ -905,4 +907,19 @@ public abstract class Objects { return sb.toString(); } + public static void nullSafeClose(Closeable... closeables) { + if (closeables == null) { + return; + } + + for (Closeable closeable : closeables) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + //Ignore the exception during close. + } + } + } + } } diff --git a/src/main/java/io/jsonwebtoken/lang/Strings.java b/src/main/java/io/jsonwebtoken/lang/Strings.java index 3fa1b6ca..8e1157f4 100644 --- a/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.lang; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -40,6 +41,8 @@ public abstract class Strings { private static final char EXTENSION_SEPARATOR = '.'; + public static final Charset UTF_8 = Charset.forName("UTF-8"); + //--------------------------------------------------------------------- // General convenience methods for working with Strings //--------------------------------------------------------------------- diff --git a/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy new file mode 100644 index 00000000..f588ebbd --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/CompressionExceptionTest.groovy @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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 + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class CompressionExceptionTest { + + @Test + void testDefaultConstructor() { + def exception = new CompressionException("my message") + + assertEquals "my message", exception.getMessage() + } + + @Test + void testConstructorWithCause() { + + def ioException = new IOException("root error") + + def exception = new CompressionException("wrapping", ioException) + + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} \ No newline at end of file diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index b8e6306a..c20ceda4 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.TextCodec +import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.RsaProvider @@ -186,6 +187,16 @@ class JwtsTest { } } + @Test + void testWithInvalidCompressionAlgorithm() { + try { + + Jwts.builder().setHeaderParam(Header.COMPRESSION_ALGORITHM, "CUSTOM").setId("andId").compact() + } catch (CompressionException e) { + assertEquals "Unsupported compression algorithm 'CUSTOM'", e.getMessage() + } + } + @Test void testConvenienceIssuer() { String compact = Jwts.builder().setIssuer("Me").compact(); @@ -320,6 +331,67 @@ class JwtsTest { assertNull claims.getId() } + @Test + void testCompressedJwtWithDeflate() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.DEFLATE).compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "DEF", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + + @Test + void testCompressedJwtWithGZIP() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.GZIP).compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "GZIP", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + + @Test + void testCompressStringPayloadWithDeflate() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String payload = "this is my test for a payload" + + String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key) + .compressWith(CompressionCodecs.DEFLATE).compact() + + def jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) + + String parsed = jws.body + + assertEquals "DEF", jws.header.getCompressionAlgorithm() + + assertEquals "this is my test for a payload", parsed + } + @Test void testHS256() { testHmac(SignatureAlgorithm.HS256); diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 7090d03c..4ee7d8e5 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -173,7 +173,7 @@ class DefaultJwtBuilderTest { def b = new DefaultJwtBuilder() { @Override - protected String toJson(Object o) throws JsonProcessingException { + protected byte[] toJson(Object o) throws JsonProcessingException { throw new JsonMappingException('foo') } } From 6401727b2a0c263b81c4d465cdace778efc137b5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:03:07 -0400 Subject: [PATCH 22/50] Refactored generic get with required type to handle the official claim types that are dates. Updated javadocs to reflect 'require' language. --- src/main/java/io/jsonwebtoken/JwtParser.java | 34 +++++++++++-------- .../io/jsonwebtoken/impl/DefaultClaims.java | 13 +++++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 22 ++++++------ .../impl/DefaultClaimsTest.groovy | 29 ++++++++++++++-- 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 828563ea..44019f77 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -28,15 +28,17 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; /** - * Sets an expected value for the jti claim. + * Ensures that the specified {@code jti} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param id - * @return the parser for method chaining. + * @return the parser method for chaining. */ JwtParser requireId(String id); /** - * Sets an expected value for the subject claim. + * Ensures that the specified {@code sub} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param subject * @return the parser for method chaining. @@ -44,7 +46,8 @@ public interface JwtParser { JwtParser requireSubject(String subject); /** - * Sets an expected value for the audience claim. + * Ensures that the specified {@code aud} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param audience * @return the parser for method chaining. @@ -52,7 +55,8 @@ public interface JwtParser { JwtParser requireAudience(String audience); /** - * Sets an expected value for the issuer claim. + * Ensures that the specified {@code iss} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param issuer * @return the parser for method chaining. @@ -60,7 +64,8 @@ public interface JwtParser { JwtParser requireIssuer(String issuer); /** - * Sets an expected value for the issuedAt claim. + * Ensures that the specified {@code iat} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param issuedAt * @return the parser for method chaining. @@ -68,7 +73,8 @@ public interface JwtParser { JwtParser requireIssuedAt(Date issuedAt); /** - * Sets an expected value for the expiration claim. + * Ensures that the specified {@code exp} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param expiration * @return the parser for method chaining. @@ -76,7 +82,8 @@ public interface JwtParser { JwtParser requireExpiration(Date expiration); /** - * Sets an expected value for the notBefore claim. + * Ensures that the specified {@code nbf} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * * @param notBefore * @return the parser for method chaining @@ -84,15 +91,14 @@ public interface JwtParser { JwtParser requireNotBefore(Date notBefore); /** - * Sets an expected value for any given claim name. + * Ensures that the specified {@code claimName} value is present when parsing the JWT. If not present, + * an exception will be thrown indicating that the JWT is invalid and may not be used. * - * If an expectation is set for a particular claim name and the JWT being parsed does not have that claim set, + * If a particular claim is required and the JWT being parsed does not have that claim set, * a {@Link MissingClaimException} will be thrown. * - * If an expectation is set for a particular claim name and the JWT being parsed has a value that is different than - * the expected value, a {@link IncorrectClaimException} will be thrown. - * - * If either {@code claimName} is null or empty or {@code value} is null, the expectation is simply ignored. + * If a particular claim is required and the JWT being parsed has a value that is different than + * the required value, a {@link IncorrectClaimException} will be thrown. * * @param claimName * @param value diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 38df87ee..196c82ff 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -66,7 +66,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getExpiration() { - return getDate(Claims.EXPIRATION); + return get(Claims.EXPIRATION, Date.class); } @Override @@ -77,7 +77,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getNotBefore() { - return getDate(Claims.NOT_BEFORE); + return get(Claims.NOT_BEFORE, Date.class); } @Override @@ -88,7 +88,7 @@ public class DefaultClaims extends JwtMap implements Claims { @Override public Date getIssuedAt() { - return getDate(Claims.ISSUED_AT); + return get(Claims.ISSUED_AT, Date.class); } @Override @@ -113,6 +113,13 @@ public class DefaultClaims extends JwtMap implements Claims { Object value = get(claimName); if (value == null) { return null; } + if (Claims.EXPIRATION.equals(claimName) || + Claims.ISSUED_AT.equals(claimName) || + Claims.NOT_BEFORE.equals(claimName) + ) { + value = getDate(claimName); + } + if (requiredType == Date.class && value instanceof Long) { value = new Date((Long)value); } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 9bbe3b2b..a759549d 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -381,16 +381,18 @@ public class DefaultJwtParser implements JwtParser { Object expectedClaimValue = expectedClaims.get(expectedClaimName); Object actualClaimValue = claims.get(expectedClaimName); - if (Claims.ISSUED_AT.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getIssuedAt(); - actualClaimValue = claims.getIssuedAt(); - } else if (Claims.EXPIRATION.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getExpiration(); - actualClaimValue = claims.getExpiration(); - } else if (Claims.NOT_BEFORE.equals(expectedClaimName)) { - expectedClaimValue = expectedClaims.getNotBefore(); - actualClaimValue = claims.getNotBefore(); - } else if (expectedClaimValue instanceof Date && actualClaimValue != null && actualClaimValue instanceof Long) { + if ( + Claims.ISSUED_AT.equals(expectedClaimName) || + Claims.EXPIRATION.equals(expectedClaimName) || + Claims.NOT_BEFORE.equals(expectedClaimName) + ) { + expectedClaimValue = expectedClaims.get(expectedClaimName, Date.class); + actualClaimValue = claims.get(expectedClaimName, Date.class); + } else if ( + expectedClaimValue instanceof Date && + actualClaimValue != null && + actualClaimValue instanceof Long + ) { actualClaimValue = new Date((Long)actualClaimValue); } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index cb584a4d..161957f0 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -64,7 +64,7 @@ class DefaultClaimsTest { def actual = new Date(); claims.put("aDate", actual) Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual); + assertEquals(expected, actual) } @Test @@ -73,6 +73,31 @@ class DefaultClaimsTest { // note that Long is stored in claim claims.put("aDate", actual.getTime()) Date expected = claims.get("aDate", Date.class); - assertEquals(expected, actual); + assertEquals(expected, actual) } + + @Test + void testGetClaimExpiration_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setExpiration(now) + Date expected = claims.get("exp", Date.class) + assertEquals(expected, claims.getExpiration()) + } + + @Test + void testGetClaimIssuedAt_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setIssuedAt(now) + Date expected = claims.get("iat", Date.class) + assertEquals(expected, claims.getIssuedAt()) + } + + @Test + void testGetClaimNotBefore_Success() { + def now = new Date(System.currentTimeMillis()) + claims.setNotBefore(now) + Date expected = claims.get("nbf", Date.class) + assertEquals(expected, claims.getNotBefore()) + } + } From 681a3fc0bad92e61a15f4df36c7d10b7ebcc4dd7 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:20:02 -0400 Subject: [PATCH 23/50] Added coverage test for JwtMap. --- src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 1b155b3e..0d00f6d7 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -20,6 +20,12 @@ import static org.junit.Assert.* class JwtMapTest { + @Test + void testToDateFromNull() { + Date actual = JwtMap.toDate(null, 'foo') + assertNull actual + } + @Test void testToDateFromDate() { From a22a76ad79d202798a2f36518cb7a7288df56ff2 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Wed, 23 Sep 2015 17:40:50 -0400 Subject: [PATCH 24/50] Update to javadocs to make more clear. --- src/main/java/io/jsonwebtoken/JwtParser.java | 62 +++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 44019f77..dd9b1271 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -28,81 +28,99 @@ public interface JwtParser { public static final char SEPARATOR_CHAR = '.'; /** - * Ensures that the specified {@code jti} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code jti} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param id * @return the parser method for chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireId(String id); /** - * Ensures that the specified {@code sub} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code sub} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param subject * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireSubject(String subject); /** - * Ensures that the specified {@code aud} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code aud} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param audience * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireAudience(String audience); /** - * Ensures that the specified {@code iss} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code iss} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param issuer * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireIssuer(String issuer); /** - * Ensures that the specified {@code iat} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code iat} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param issuedAt * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireIssuedAt(Date issuedAt); /** - * Ensures that the specified {@code exp} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code exp} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param expiration * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireExpiration(Date expiration); /** - * Ensures that the specified {@code nbf} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. + * Ensures that the specified {@code nbf} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param notBefore * @return the parser for method chaining + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser requireNotBefore(Date notBefore); /** - * Ensures that the specified {@code claimName} value is present when parsing the JWT. If not present, - * an exception will be thrown indicating that the JWT is invalid and may not be used. - * - * If a particular claim is required and the JWT being parsed does not have that claim set, - * a {@Link MissingClaimException} will be thrown. - * - * If a particular claim is required and the JWT being parsed has a value that is different than - * the required value, a {@link IncorrectClaimException} will be thrown. + * Ensures that the specified {@code claimName} exists in the parsed JWT. If missing or if the parsed + * value does not equal the specified value, an exception will be thrown indicating that the + * JWT is invalid and may not be used. * * @param claimName * @param value * @return the parser for method chaining. + * @see MissingClaimException + * @see IncorrectClaimException */ JwtParser require(String claimName, Object value); From 137b6db31d1876b967238491ff04d744ea96b2ac Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 23 Sep 2015 15:05:30 -0700 Subject: [PATCH 25/50] #42: Added readme/release documentation for required JWT field value functionality. --- README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ccdb2b3..ccac0a77 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,43 @@ These feature sets will be implemented in a future release when possible. Commu ### 0.6 -- Added the ability to set expectations when parsing a JWT which enforces a particular claim having a particular value +#### Enforce JWT Values when Parsing + +We added the ability to set expectations when parsing a JWT which ensures particular claims having particular values. + +For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, +otherwise you may not trust the token. You can do that by using one of the `require` methods on the parser builder: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the sub field was missing or did not have a 'jsmith' value +} +``` + +If it is important to react to a missing vs an incorrect value, instead of catching `InvalidClaimException`, you can catch either `MissingClaimException` or `IncorrectClaimException`: + +```java +try { + Jwts.parser().requireSubject("jsmith").setSigningKey(key).parseClaimsJws(s); +} catch(MissingClaimException mce) { + // the parsed JWT did not have the sub field +} catch(IncorrectClaimException ice) { + // the parsed JWT had a sub field, but its value was not equal to 'jsmith' +} +``` + +You can also require custom fields by using the `require(name, requiredValue)` method - for example: + +```java +try { + Jwts.parser().require("myfield", "myRequiredValue").setSigningKey(key).parseClaimsJws(s); +} catch(InvalidClaimException ice) { + // the 'myfield' field was missing or did not have a 'myRequiredValue' value +} +``` +(or, again, you could catch either MissingClaimException or IncorrectClaimException instead) ### 0.5.1 From 3e80cd647b59effb91aaf1b14e082345466b05a0 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 23 Sep 2015 15:07:46 -0700 Subject: [PATCH 26/50] Minor readability update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccac0a77..655a333f 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ try { } ``` -You can also require custom fields by using the `require(name, requiredValue)` method - for example: +You can also require custom fields by using the `require(fieldName, requiredFieldValue)` method - for example: ```java try { From 806844a89a279f8e57fbee9121a03695cf68215b Mon Sep 17 00:00:00 2001 From: Jason Erickson Date: Wed, 23 Sep 2015 15:44:07 -0700 Subject: [PATCH 27/50] Issue-52: Refactoring and adding unit tests to cover the compression functionality --- .../io/jsonwebtoken/CompressionCodec.java | 18 ++++- .../CompressionCodecResolver.java | 8 +- .../compression/BaseCompressionCodec.java | 79 ++++++++++++++++++ .../impl/compression/CompressionCodecs.java | 15 ++-- .../DefaultCompressionCodecResolver.java | 18 +++-- .../compression/DeflateCompressionCodec.java | 19 +---- .../compression/GzipCompressionCodec.java | 44 ++++------ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 80 +++++++++++++++++++ .../BaseCompressionCodecTest.groovy | 55 +++++++++++++ 9 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java create mode 100644 src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index e6a24481..84c56a29 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -16,15 +16,29 @@ package io.jsonwebtoken; /** - * CompressionCodec + * Defines how to compress and decompress byte arrays. * - * @since 0.59 + * @since 0.5.2 */ public interface CompressionCodec { + /** + * The algorithm name that would appear in the JWT header. + * @return the algorithm name that would appear in the JWT header + */ String getAlgorithmName(); + /** + * Takes a byte array and returns a compressed version. + * @param payload bytes to compress + * @return compressed bytes + */ byte[] compress(byte[] payload); + /** + * Takes a compressed byte array and returns a decompressed version. + * @param compressed compressed bytes + * @return decompressed bytes + */ byte[] decompress(byte[] compressed); } \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 76805e06..877ab890 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -16,12 +16,16 @@ package io.jsonwebtoken; /** - * CompressionCodecResolver + * Resolves "calg" header to an implementation of CompressionCodec. * * @since 0.5.2 */ public interface CompressionCodecResolver { - + /** + * Examines the header and returns a CompressionCodec if it finds one that it recognizes. + * @param header of the JWT + * @return CompressionCodec matching the "calg" header, or null if there is no "calg" header. + */ CompressionCodec resolveCompressionCodec(Header header); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java new file mode 100644 index 00000000..1c86613a --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.impl.compression; + +import io.jsonwebtoken.CompressionCodec; +import io.jsonwebtoken.CompressionException; +import io.jsonwebtoken.lang.Assert; + +import java.io.IOException; + +/** + * Base class that asserts arguments and wraps IOException with CompressionException. + * + * @since 0.5.2 + */ +public abstract class BaseCompressionCodec implements CompressionCodec { + /** + * Implement this method to do the actual work of compressing the payload + * @param payload the bytes to compress + * @return the compressed bytes + * @throws IOException if the compression causes an IOException + */ + protected abstract byte[] doCompress(byte[] payload) throws IOException; + + /** + * Asserts that payload is not null and calls doCompress + * @param payload bytes to compress + * @return compressed bytes + * @throws CompressionException if doCompress throws an IOException + */ + @Override + public final byte[] compress(byte[] payload) { + Assert.notNull(payload, "payload cannot be null."); + + try { + return doCompress(payload); + } catch (IOException e) { + throw new CompressionException("Unable to compress payload.", e); + } + } + + /** + * Asserts the compressed bytes is not null and calls doDecompress + * @param compressed compressed bytes + * @return decompressed bytes + * @throws CompressionException if doCompress throws an IOException + */ + @Override + public final byte[] decompress(byte[] compressed) { + Assert.notNull(compressed, "compressed bytes cannot be null."); + + try { + return doDecompress(compressed); + } catch (IOException e) { + throw new CompressionException("Unable to decompress bytes.", e); + } + } + + /** + * Implement this method to do the actual work of decompressing the compressed bytes. + * @param compressed compressed bytes + * @return decompressed bytes + * @throws IOException if the decompression runs into an IO problem + */ + protected abstract byte[] doDecompress(byte[] compressed) throws IOException; +} diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java index d1d663d3..8a1fd7eb 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -22,13 +22,16 @@ import io.jsonwebtoken.CompressionCodec; * * @since 0.5.2 */ -public abstract class CompressionCodecs { +public interface CompressionCodecs { - private CompressionCodecs(){} - - public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); - - public static final CompressionCodec GZIP = new GzipCompressionCodec(); + /** + * Codec implementing the deflate compression algorithm + */ + CompressionCodec DEFLATE = new DeflateCompressionCodec(); + /** + * Codec implementing the gzip compression algorithm + */ + CompressionCodec GZIP = new GzipCompressionCodec(); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 00b37a88..298c3b57 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -23,7 +23,8 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; /** - * DefaultCompressionCodecResolver + * Default implementation of {@link CompressionCodecResolver}. This implementation will resolve DEF to + * {@link DeflateCompressionCodec} and GZIP to {@link GzipCompressionCodec}. * * @since 0.5.2 */ @@ -31,22 +32,25 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver @Override public CompressionCodec resolveCompressionCodec(Header header) { - Assert.notNull(header, "header cannot be null."); + String cmpAlg = getAlgorithmFromHeader(header); - String cmpAlg = header.getCompressionAlgorithm(); - - if (!Strings.hasText(cmpAlg)) { + final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg); + if (!hasCompressionAlgorithm) { return null; } - if (CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { return CompressionCodecs.DEFLATE; } - if (CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) { return CompressionCodecs.GZIP; } throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); } + + protected final String getAlgorithmFromHeader(Header header) { + Assert.notNull(header, "header cannot be null."); + + return header.getCompressionAlgorithm(); + } } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index 83b5725d..e7390fbd 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -15,9 +15,6 @@ */ package io.jsonwebtoken.impl.compression; -import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionException; -import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; import java.io.ByteArrayOutputStream; @@ -27,11 +24,10 @@ import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterOutputStream; /** - * DeflateCompressionCodec - * + * Codec implementing the deflate compression algorithm * @since 0.5.2 */ -public class DeflateCompressionCodec implements CompressionCodec { +public class DeflateCompressionCodec extends BaseCompressionCodec { private static final String DEFLATE = "DEF"; @@ -41,8 +37,7 @@ public class DeflateCompressionCodec implements CompressionCodec { } @Override - public byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); + public byte[] doCompress(byte[] payload) throws IOException { Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION); @@ -55,17 +50,13 @@ public class DeflateCompressionCodec implements CompressionCodec { deflaterOutputStream.write(payload, 0, payload.length); deflaterOutputStream.flush(); return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); } finally { Objects.nullSafeClose(outputStream, deflaterOutputStream); } } @Override - public byte[] decompress(byte[] compressed) { - Assert.notNull(compressed, "compressed cannot be null."); - + public byte[] doDecompress(byte[] compressed) throws IOException { InflaterOutputStream inflaterOutputStream = null; ByteArrayOutputStream decompressedOutputStream = null; @@ -75,8 +66,6 @@ public class DeflateCompressionCodec implements CompressionCodec { inflaterOutputStream.write(compressed); inflaterOutputStream.flush(); return decompressedOutputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to decompress compressed payload.", e); } finally { Objects.nullSafeClose(decompressedOutputStream, inflaterOutputStream); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index 1a8a7a53..61fd98de 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -16,8 +16,6 @@ package io.jsonwebtoken.impl.compression; import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionException; -import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Objects; import java.io.ByteArrayInputStream; @@ -27,11 +25,11 @@ import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** - * GzipCompressionCodec + * Codec implementing the gzip compression algorithm * * @since 0.5.2 */ -public class GzipCompressionCodec implements CompressionCodec { +public class GzipCompressionCodec extends BaseCompressionCodec implements CompressionCodec { private static final String GZIP = "GZIP"; @@ -41,29 +39,7 @@ public class GzipCompressionCodec implements CompressionCodec { } @Override - public byte[] compress(byte[] payload) { - Assert.notNull(payload, "payload cannot be null."); - - ByteArrayOutputStream outputStream = null; - GZIPOutputStream gzipOutputStream = null; - - try { - outputStream = new ByteArrayOutputStream(); - gzipOutputStream = new GZIPOutputStream(outputStream, true); - gzipOutputStream.write(payload, 0, payload.length); - gzipOutputStream.finish(); - return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to compress payload.", e); - } finally { - Objects.nullSafeClose(outputStream, gzipOutputStream); - } - } - - @Override - public byte[] decompress(byte[] compressed) { - Assert.notNull(compressed, "compressed cannot be null."); - + protected byte[] doDecompress(byte[] compressed) throws IOException { byte[] buffer = new byte[512]; ByteArrayOutputStream outputStream = null; @@ -79,10 +55,20 @@ public class GzipCompressionCodec implements CompressionCodec { outputStream.write(buffer, 0, read); } return outputStream.toByteArray(); - } catch (IOException e) { - throw new CompressionException("Unable to decompress compressed payload.", e); } finally { Objects.nullSafeClose(inputStream, gzipInputStream, outputStream); } } + + protected byte[] doCompress(byte[] payload) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + GZIPOutputStream compressorOutputStream = new GZIPOutputStream(outputStream, true); + try { + compressorOutputStream.write(payload, 0, payload.length); + compressorOutputStream.finish(); + return outputStream.toByteArray(); + } finally { + Objects.nullSafeClose(compressorOutputStream, outputStream); + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c20ceda4..c188ca56 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -20,9 +20,12 @@ import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.impl.compression.CompressionCodecs +import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver +import io.jsonwebtoken.impl.compression.GzipCompressionCodec import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.impl.crypto.RsaProvider +import io.jsonwebtoken.lang.Strings import org.junit.Test import javax.crypto.Mac @@ -109,6 +112,7 @@ class JwtsTest { def token = Jwts.parser().parse(jwt); + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -331,6 +335,27 @@ class JwtsTest { assertNull claims.getId() } + @Test + void testUncompressedJwt() { + + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compact() + + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jws.body + + assertNull jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + } + @Test void testCompressedJwtWithDeflate() { @@ -373,6 +398,58 @@ class JwtsTest { assertEquals "hello this is an amazing jwt", claims.state } + @Test + void testCompressedWithCustomResolver() { + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { + @Override + String getAlgorithmName() { + return "CUSTOM" + } + }).compact() + + def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new DefaultCompressionCodecResolver() { + @Override + CompressionCodec resolveCompressionCodec(Header header) { + String algorithm = getAlgorithmFromHeader(header); + if ("CUSTOM".equals(algorithm)) { + return CompressionCodecs.GZIP + } else { + return null + } + } + }).parseClaimsJws(compact) + + Claims claims = jws.body + + assertEquals "CUSTOM", jws.header.getCompressionAlgorithm() + + assertEquals id, claims.getId() + assertEquals "an audience", claims.getAudience() + assertEquals "hello this is an amazing jwt", claims.state + + } + @Test(expected = CompressionException.class) + void testCompressedJwtWithUnrecognizedHeader() { + byte[] key = MacProvider.generateKey().getEncoded() + + String id = UUID.randomUUID().toString() + + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { + @Override + String getAlgorithmName() { + return "CUSTOM" + } + }).compact() + + Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + } + @Test void testCompressStringPayloadWithDeflate() { @@ -643,6 +720,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt); assert [alg: alg.name()] == token.header + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -657,6 +735,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt) assert token.header == [alg: alg.name()] + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } @@ -678,6 +757,7 @@ class JwtsTest { def token = Jwts.parser().setSigningKey(key).parse(jwt); assert token.header == [alg: alg.name()] + //noinspection GrEqualsBetweenInconvertibleTypes assert token.body == claims } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy new file mode 100644 index 00000000..7ffc8eca --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 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.impl.compression + +import io.jsonwebtoken.CompressionCodec +import io.jsonwebtoken.CompressionException +import org.junit.Test + +/** + * @since 0.5.2 + */ +class BaseCompressionCodecTest { + static class ExceptionThrowingCodec extends BaseCompressionCodec { + + @Override + protected byte[] doCompress(byte[] payload) throws IOException { + throw new IOException("Test Exception") + } + + @Override + String getAlgorithmName() { + return "Test" + } + + @Override + protected byte[] doDecompress(byte[] payload) throws IOException { + throw new IOException("Test Decompress Exception"); + } + } + + @Test(expected = CompressionException.class) + void testCompressWithException() { + CompressionCodec codecUT = new ExceptionThrowingCodec(); + codecUT.compress(new byte[0]); + } + + @Test(expected = CompressionException.class) + void testDecompressWithException() { + CompressionCodec codecUT = new ExceptionThrowingCodec(); + codecUT.decompress(new byte[0]); + } +} From 7e15e2de022d7c110c7524cbd7ab9ef5b2c12bab Mon Sep 17 00:00:00 2001 From: Jason Erickson Date: Wed, 23 Sep 2015 17:24:47 -0700 Subject: [PATCH 28/50] Issue-52: Refactoring and adding unit tests to cover the compression functionality --- .../impl/compression/DefaultCompressionCodecResolver.java | 2 +- src/test/groovy/io/jsonwebtoken/JwtsTest.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 298c3b57..4dde29d5 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -48,7 +48,7 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'"); } - protected final String getAlgorithmFromHeader(Header header) { + private String getAlgorithmFromHeader(Header header) { Assert.notNull(header, "header cannot be null."); return header.getCompressionAlgorithm(); diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c188ca56..9a344f0b 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -415,7 +415,7 @@ class JwtsTest { def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new DefaultCompressionCodecResolver() { @Override CompressionCodec resolveCompressionCodec(Header header) { - String algorithm = getAlgorithmFromHeader(header); + String algorithm = header.getCompressionAlgorithm() if ("CUSTOM".equals(algorithm)) { return CompressionCodecs.GZIP } else { From fef553ad72ef0c2aa49ea3f5abddd23f08668657 Mon Sep 17 00:00:00 2001 From: josebarrueta Date: Fri, 9 Oct 2015 18:07:06 -0700 Subject: [PATCH 29/50] Issue-52 Improving Javadoc for compression --- .../io/jsonwebtoken/CompressionCodec.java | 2 ++ src/main/java/io/jsonwebtoken/Header.java | 19 ++++++++++++++++++- src/main/java/io/jsonwebtoken/JwtBuilder.java | 5 ++++- src/main/java/io/jsonwebtoken/JwtParser.java | 4 +++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index 84c56a29..35f03886 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -19,6 +19,8 @@ package io.jsonwebtoken; * Defines how to compress and decompress byte arrays. * * @since 0.5.2 + * @see io.jsonwebtoken.impl.compression.DeflateCompressionCodec + * @see io.jsonwebtoken.impl.compression.GzipCompressionCodec */ public interface CompressionCodec { diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index f4b5cfbf..81068c73 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -103,8 +103,25 @@ public interface Header> extends Map { */ T setContentType(String cty); + /** + * Returns the JWT calg (Compression Algorithm) header value or {@code null} if not present. + * + * @return the {@code calg} header parameter value or {@code null} if not present. + * @since 0.5.2 + */ String getCompressionAlgorithm(); - T setCompressionAlgorithm(String compressionAlgorithm); + /** + * Sets the JWT calg (Compression Algorithm) header parameter value. A {@code null} value will remove + * the property from the JSON map. + *

+ *

The compression algorithm is NOT part of the JJWT's default {@link JwtParser} implementation supports both the + * {@link io.jsonwebtoken.impl.compression.DeflateCompressionCodec DEFLATE} + * and {@link io.jsonwebtoken.impl.compression.GzipCompressionCodec GZIP} algorithms by default - you do not need to + * specify a {@code CompressionCodecResolver} in these cases.

+ * + *

However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement + * your own {@link CompressionCodecResolver} and specify that when + * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} and + * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

* * @since 0.5.2 */ public interface CompressionCodecResolver { + /** - * Examines the header and returns a CompressionCodec if it finds one that it recognizes. + * Looks for a JWT {@code calg} header, and if found, returns the corresponding {@link CompressionCodec} the parser + * can use to decompress the JWT body. + * * @param header of the JWT - * @return CompressionCodec matching the "calg" header, or null if there is no "calg" header. + * @return CompressionCodec matching the {@code calg} header, or null if there is no {@code calg} header. + * @throws CompressionException if a {@code calg} header value is found and not supported. */ - CompressionCodec resolveCompressionCodec(Header header); + CompressionCodec resolveCompressionCodec(Header header) throws CompressionException; } diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java index b7985ef3..f88d2b0f 100644 --- a/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -350,10 +350,18 @@ public interface JwtBuilder extends ClaimsMutator { JwtBuilder signWith(SignatureAlgorithm alg, Key key); /** - * Compresses the JWT body using the {@link CompressionCodec} passed as argument. + * Compresses the JWT body using the specified {@link CompressionCodec}. * - * Note: Compression is not part of the Json Web Token specification and is not expected that other libraries (including - * older versions of this one) are able to consume a compressed JWT body correctly. + *

If your compact JWTs are large, and you want to reduce their total size during network transmission, this + * can be useful. For example, when embedding JWTs in URLs, some browsers may not support URLs longer than a + * certain length. Using compression can help ensure the compact JWT fits within that length. However, NOTE:

+ * + *

WARNING: Compression is not defined by the JWT Specification, and it is not expected that other libraries + * (including JJWT versions < 0.5.2) are able to consume a compressed JWT body correctly. Only use this method + * if you are sure that you will consume the JWT with JJWT >= 0.5.2 or another library that you know implements + * the same behavior.

+ * + * @see io.jsonwebtoken.impl.compression.CompressionCodecs * * @param codec implementation of the {@link CompressionCodec} to be used. * @return the builder for method chaining. diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 5e5dcabd..88daf131 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -127,10 +127,10 @@ public interface JwtParser { /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. - * + *

*

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

- * + *

*

This method overwrites any previously set key.

* * @param key the algorithm-specific signature verification key used to validate any discovered JWS digital @@ -142,12 +142,12 @@ public interface JwtParser { /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. - * + *

*

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

- * + *

*

This method overwrites any previously set key.

- * + *

*

This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting * byte array is used to invoke {@link #setSigningKey(byte[])}.

* @@ -160,12 +160,12 @@ public interface JwtParser { /** * Sets the signing key used to verify any discovered JWS digital signature. If the specified JWT string is not * a JWS (no signature), this key is not used. - * + *

*

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

- * + *

*

This method overwrites any previously set key.

- * + *

*

This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting * byte array is used to invoke {@link #setSigningKey(byte[])}.

* @@ -178,12 +178,12 @@ public interface JwtParser { /** * 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
@@ -193,9 +193,9 @@ public interface JwtParser {
      *         }})
      *     .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.

* @@ -206,10 +206,22 @@ public interface JwtParser { JwtParser setSigningKeyResolver(SigningKeyResolver signingKeyResolver); /** - * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to verify - * a decompress the JWT body. If the parsed JWT is not compressed, this resolver si not used. + * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to + * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. + *

NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries + * (including JJWT versions < 0.5.2) are able to consume a compressed JWT body correctly. This method is only + * useful if the compact JWT was compressed with JJWT >= 0.5.2 or another library that you know implements + * the same behavior.

+ *
Default Support
+ *

JJWT's default {@link JwtParser} implementation supports both the + * {@link io.jsonwebtoken.impl.compression.DeflateCompressionCodec DEFLATE} + * and {@link io.jsonwebtoken.impl.compression.GzipCompressionCodec GZIP} algorithms by default - you do not need to + * specify a {@code CompressionCodecResolver} in these cases.

+ *

However, if you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement + * your own {@link CompressionCodecResolver} and specify that via this method and also when + * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} JWTs.

* - * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. + * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser for method chaining. * @since 0.5.2 */ @@ -218,7 +230,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 more efficient to attempt to * parse the token (and catching exceptions if necessary) instead of calling this method first before parsing.

* @@ -231,7 +243,7 @@ public interface JwtParser { /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and * returns the resulting JWT or JWS instance. - * + *

*

This method returns a JWT or JWS based on the parsed string. Because it may be cumbersome to determine if it * is a JWT or JWS, or if the body/payload is a Claims or String with {@code instanceof} checks, the * {@link #parse(String, JwtHandler) parse(String,JwtHandler)} method allows for a type-safe callback approach that @@ -258,11 +270,11 @@ public interface JwtParser { /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and * invokes the specified {@code handler} with the resulting JWT or JWS instance. - * + *

*

If you are confident of the format of the JWT before parsing, you can create an anonymous subclass using the * {@link io.jsonwebtoken.JwtHandlerAdapter JwtHandlerAdapter} and override only the methods you know are relevant * for your use case(s), for example:

- * + *

*

      * String compactJwt = request.getParameter("jwt"); //we are confident this is a signed JWS
      *
@@ -273,10 +285,10 @@ public interface JwtParser {
      *     }
      * });
      * 
- * + *

*

If you know the JWT string can be only one type of JWT, then it is even easier to invoke one of the * following convenience methods instead of this one:

- * + *

*

    *
  • {@link #parsePlaintextJwt(String)}
  • *
  • {@link #parseClaimsJwt(String)}
  • @@ -302,17 +314,17 @@ public interface JwtParser { * @since 0.2 */ T parse(String jwt, JwtHandler handler) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and * returns * the resulting unsigned plaintext JWT instance. - * + *

    *

    This is a convenience method that is usable if you are confident that the compact string argument reflects an * unsigned plaintext JWT. An unsigned plaintext JWT has a String (non-JSON) body payload and it is not * cryptographically signed.

    - * + *

    *

    If the compact string presented does not reflect an unsigned plaintext JWT with non-JSON string body, * an {@link UnsupportedJwtException} will be thrown.

    * @@ -332,17 +344,17 @@ public interface JwtParser { * @since 0.2 */ Jwt parsePlaintextJwt(String plaintextJwt) - throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWT string based on the builder's current configuration state and * returns * the resulting unsigned plaintext JWT instance. - * + *

    *

    This is a convenience method that is usable if you are confident that the compact string argument reflects an * unsigned Claims JWT. An unsigned Claims JWT has a {@link Claims} body and it is not cryptographically * signed.

    - * + *

    *

    If the compact string presented does not reflect an unsigned Claims JWT, an * {@link UnsupportedJwtException} will be thrown.

    * @@ -363,17 +375,17 @@ public interface JwtParser { * @since 0.2 */ Jwt parseClaimsJwt(String claimsJwt) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWS string based on the builder's current configuration state and * returns * the resulting plaintext JWS instance. - * + *

    *

    This is a convenience method that is usable if you are confident that the compact string argument reflects a * plaintext JWS. A plaintext JWS is a JWT with a String (non-JSON) body (payload) that has been * cryptographically signed.

    - * + *

    *

    If the compact string presented does not reflect a plaintext JWS, an {@link UnsupportedJwtException} * will be thrown.

    * @@ -391,16 +403,16 @@ public interface JwtParser { * @since 0.2 */ Jws parsePlaintextJws(String plaintextJws) - throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; /** * Parses the specified compact serialized JWS string based on the builder's current configuration state and * returns * the resulting Claims JWS instance. - * + *

    *

    This is a convenience method that is usable if you are confident that the compact string argument reflects a * Claims JWS. A Claims JWS is a JWT with a {@link Claims} body that has been cryptographically signed.

    - * + *

    *

    If the compact string presented does not reflect a Claims JWS, an {@link UnsupportedJwtException} will be * thrown.

    * @@ -420,5 +432,5 @@ public interface JwtParser { * @since 0.2 */ Jws parseClaimsJws(String claimsJws) - throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; + throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException; } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index df5d654f..a5d68aec 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -26,13 +26,12 @@ import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.InvalidClaimException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.MissingClaimException; -import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtHandler; import io.jsonwebtoken.JwtHandlerAdapter; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.PrematureJwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; diff --git a/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java similarity index 79% rename from src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java rename to src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index 1c86613a..9457b68b 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/BaseCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -22,13 +22,15 @@ import io.jsonwebtoken.lang.Assert; import java.io.IOException; /** - * Base class that asserts arguments and wraps IOException with CompressionException. + * Abstract class that asserts arguments and wraps IOException with CompressionException. * * @since 0.5.2 */ -public abstract class BaseCompressionCodec implements CompressionCodec { +public abstract class AbstractCompressionCodec implements CompressionCodec { + /** * Implement this method to do the actual work of compressing the payload + * * @param payload the bytes to compress * @return the compressed bytes * @throws IOException if the compression causes an IOException @@ -36,10 +38,11 @@ public abstract class BaseCompressionCodec implements CompressionCodec { protected abstract byte[] doCompress(byte[] payload) throws IOException; /** - * Asserts that payload is not null and calls doCompress + * Asserts that payload is not null and calls {@link #doCompress(byte[]) doCompress} + * * @param payload bytes to compress * @return compressed bytes - * @throws CompressionException if doCompress throws an IOException + * @throws CompressionException if {@link #doCompress(byte[]) doCompress} throws an IOException */ @Override public final byte[] compress(byte[] payload) { @@ -53,10 +56,11 @@ public abstract class BaseCompressionCodec implements CompressionCodec { } /** - * Asserts the compressed bytes is not null and calls doDecompress + * Asserts the compressed bytes is not null and calls {@link #doDecompress(byte[]) doDecompress} + * * @param compressed compressed bytes * @return decompressed bytes - * @throws CompressionException if doCompress throws an IOException + * @throws CompressionException if {@link #doDecompress(byte[]) doDecompress} throws an IOException */ @Override public final byte[] decompress(byte[] compressed) { @@ -71,6 +75,7 @@ public abstract class BaseCompressionCodec implements CompressionCodec { /** * Implement this method to do the actual work of decompressing the compressed bytes. + * * @param compressed compressed bytes * @return decompressed bytes * @throws IOException if the decompression runs into an IO problem diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java index 8a1fd7eb..f81721cd 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -18,20 +18,27 @@ package io.jsonwebtoken.impl.compression; import io.jsonwebtoken.CompressionCodec; /** - * CompressionCodecs exposes default implementation of the {@link CompressionCodec} interface. + * Provides default implementations of the {@link CompressionCodec} interface. + * + * @see #DEFLATE + * @see #GZIP * * @since 0.5.2 */ -public interface CompressionCodecs { +public final class CompressionCodecs { + + private static final CompressionCodecs I = new CompressionCodecs(); + + private CompressionCodecs(){} //prevent external instantiation /** * Codec implementing the
    deflate compression algorithm */ - CompressionCodec DEFLATE = new DeflateCompressionCodec(); + public static final CompressionCodec DEFLATE = new DeflateCompressionCodec(); /** * Codec implementing the gzip compression algorithm */ - CompressionCodec GZIP = new GzipCompressionCodec(); + public static final CompressionCodec GZIP = new GzipCompressionCodec(); } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 4dde29d5..159a6f44 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -23,9 +23,24 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; /** - * Default implementation of {@link CompressionCodecResolver}. This implementation will resolve DEF to - * {@link DeflateCompressionCodec} and GZIP to {@link GzipCompressionCodec}. + * Default implementation of {@link CompressionCodecResolver} that supports the following: + *

    + *

      + *
    • If the specified JWT {@link Header} does not have a {@code calg} header, this implementation does + * nothing and returns {@code null} to the caller, indicating no compression was used.
    • + *
    • If the header has a {@code calg} value of {@code DEF}, a {@link DeflateCompressionCodec} will be returned.
    • + *
    • If the header has a {@code calg} value of {@code GZIP}, a {@link GzipCompressionCodec} will be returned.
    • + *
    • If the header has any other {@code calg} value, a {@link CompressionException} is thrown to reflect an + * unrecognized algorithm.
    • + *
    * + *

    If you want to use a compression algorithm other than {@code DEF} or {@code GZIP}, you must implement your own + * {@link CompressionCodecResolver} and specify that when + * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} and + * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    + * + * @see DeflateCompressionCodec + * @see GzipCompressionCodec * @since 0.5.2 */ public class DefaultCompressionCodecResolver implements CompressionCodecResolver { @@ -35,6 +50,7 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver String cmpAlg = getAlgorithmFromHeader(header); final boolean hasCompressionAlgorithm = Strings.hasText(cmpAlg); + if (!hasCompressionAlgorithm) { return null; } diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index e7390fbd..90ee9dbc 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -24,10 +24,11 @@ import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterOutputStream; /** - * Codec implementing the deflate compression algorithm + * Codec implementing the deflate compression algorithm. + * * @since 0.5.2 */ -public class DeflateCompressionCodec extends BaseCompressionCodec { +public class DeflateCompressionCodec extends AbstractCompressionCodec { private static final String DEFLATE = "DEF"; diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index 61fd98de..f15531e1 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -25,11 +25,11 @@ import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** - * Codec implementing the gzip compression algorithm + * Codec implementing the gzip compression algorithm. * * @since 0.5.2 */ -public class GzipCompressionCodec extends BaseCompressionCodec implements CompressionCodec { +public class GzipCompressionCodec extends AbstractCompressionCodec implements CompressionCodec { private static final String GZIP = "GZIP"; diff --git a/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy similarity index 93% rename from src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy rename to src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index 7ffc8eca..dc497c37 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/compression/BaseCompressionCodecTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -22,8 +22,8 @@ import org.junit.Test /** * @since 0.5.2 */ -class BaseCompressionCodecTest { - static class ExceptionThrowingCodec extends BaseCompressionCodec { +class AbstractCompressionCodecTest { + static class ExceptionThrowingCodec extends AbstractCompressionCodec { @Override protected byte[] doCompress(byte[] payload) throws IOException { From 4d230a07256401a170911786de180886db28a003 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 14:17:13 -0700 Subject: [PATCH 31/50] #58: added toString implementations for JwtMap, DefaultJwt and DefaultJws with tests --- .../java/io/jsonwebtoken/impl/DefaultJws.java | 5 +++++ .../java/io/jsonwebtoken/impl/DefaultJwt.java | 5 +++++ src/main/java/io/jsonwebtoken/impl/JwtMap.java | 5 +++++ .../io/jsonwebtoken/impl/DefaultJwsTest.groovy | 13 +++++++++++++ .../io/jsonwebtoken/impl/DefaultJwtTest.groovy | 18 ++++++++++++++++++ 5 files changed, 46 insertions(+) create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index f1b4958b..fe83244c 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -44,4 +44,9 @@ public class DefaultJws implements Jws { public String getSignature() { return this.signature; } + + @Override + public String toString() { + return "header=" + header + ",body=" + body + ",signature=" + signature; + } } diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java index d5ed16c9..e09bd006 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwt.java @@ -37,4 +37,9 @@ public class DefaultJwt implements Jwt { public B getBody() { return body; } + + @Override + public String toString() { + return "header=" + header + ",body=" + body; + } } diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 7c2de6d4..40d21224 100644 --- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -150,4 +150,9 @@ public class JwtMap implements Map { public Set> entrySet() { return map.entrySet(); } + + @Override + public String toString() { + return map.toString(); + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index 3acaf314..33f0d97a 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -17,6 +17,8 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.crypto.MacProvider import org.junit.Test import static org.junit.Assert.* @@ -32,4 +34,15 @@ class DefaultJwsTest { assertEquals jws.getBody(), 'foo' assertEquals jws.getSignature(), 'sig' } + + @Test + void testToString() { + //create random signing key for testing: + byte[] key = MacProvider.generateKey().encoded + String compact = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.HS256, key).compact(); + int i = compact.lastIndexOf('.') + String signature = compact.substring(i + 1) + def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + assertEquals 'header={alg=HS256},body={foo=bar},signature=' + signature, jws.toString() + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy new file mode 100644 index 00000000..e916e509 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -0,0 +1,18 @@ +package io.jsonwebtoken.impl + +import io.jsonwebtoken.Jwt +import io.jsonwebtoken.Jwts +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class DefaultJwtTest { + + @Test + void testToString() { + String compact = Jwts.builder().setHeaderParam('foo', 'bar').setAudience('jsmith').compact(); + Jwt jwt = Jwts.parser().parseClaimsJwt(compact); + assertEquals 'header={foo=bar, alg=none},body={aud=jsmith}', jwt.toString() + } + +} From 76de67fe5d3b9de7e1022732dbe72960cab00d35 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:02:15 -0700 Subject: [PATCH 32/50] updating readme to reflect 0.6 release features --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 655a333f..e0939f2a 100644 --- a/README.md +++ b/README.md @@ -101,9 +101,9 @@ These feature sets will be implemented in a future release when possible. Commu ### 0.6 -#### Enforce JWT Values when Parsing +#### Enforce JWT Claims when Parsing -We added the ability to set expectations when parsing a JWT which ensures particular claims having particular values. +You can now enforce that JWT claims have expected values when parsing a compact JWT string. For example, let's say that you require that the JWT you are parsing has a specific `sub` (subject) value, otherwise you may not trust the token. You can do that by using one of the `require` methods on the parser builder: @@ -139,6 +139,48 @@ try { ``` (or, again, you could catch either MissingClaimException or IncorrectClaimException instead) +#### Body Compression + +*This feature is NOT JWT specification compliant, but it can be very useful when you parse your own tokens.* + +If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: + +```java +Jwts.builder().claim("foo", "someReallyDataString...") + .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP + .signWith(SignatureAlgorithm.HS256, key) + .compact(); +``` + +This will set a new `calg` header with the name of the compression algorithm used so that parsers can see that value and decompress accordingly. + +The default parser implementation will automatically decompress DEFLATE or GZIP compressed bodies, so you don't need to set anything on the parser - it looks like normal: + +```java +Jwts.parser().setSigningKey(key).parseClaimsJws(compact); +``` + +##### Custom Compression Algorithms + +If the DEFLATE or GZIP algorithms are not sufficient for your needs, you can specify your own Compression algorithms by implementing the `CompressionCodec` interface and setting it on the parser: + +```java +Jwts.builder().claim("foo", "someReallyDataString...") + .compressWith(new MyCompressionCodec()) + .signWith(SignatureAlgorithm.HS256, key) + .compact(); +``` + +You will then need to specify a `CompressionCodecResolver` on the parser, so you can inspect the `calg` header and return your custom codec when discovered: + +```java +Jwts.parser().setSigningKey(key) + .setCompressionCodecResolver(new MyCustomCompressionCodecResolver()) + .parseClaimsJws(compact); +``` + +*NOTE*: Because body compression is not a standard JWT feature, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. It is best used for your own use cases - where you both create and later parse the tokens. This feature will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. + ### 0.5.1 - Minor [bug](https://github.com/jwtk/jjwt/issues/31) fix [release](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5.1+is%3Aclosed) that ensures correct Base64 padding in Android runtimes. From dad6dcf0f223873a9c73b637f401699ed1a63915 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:05:09 -0700 Subject: [PATCH 33/50] minor formatting change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0939f2a..6b91d384 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ try { #### Body Compression -*This feature is NOT JWT specification compliant, but it can be very useful when you parse your own tokens.* +**This feature is NOT JWT specification compliant*, but it can be very useful when you parse your own tokens.* If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: From 6a422211c8a16013592cbd88d0346ebe4ca3e788 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:07:01 -0700 Subject: [PATCH 34/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b91d384..249f6975 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ try { #### Body Compression -**This feature is NOT JWT specification compliant*, but it can be very useful when you parse your own tokens.* +**This feature is NOT JWT specification compliant**, *but it can be very useful when you parse your own tokens*. If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: From 65f9b02de3007ec55f441c45331e413650211382 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:07:33 -0700 Subject: [PATCH 35/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 249f6975..7193a226 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ try { #### Body Compression -**This feature is NOT JWT specification compliant**, *but it can be very useful when you parse your own tokens*. +***This feature is NOT JWT specification compliant**, but it can be very useful when you parse your own tokens*. If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: From 1649066038453d9e412698bbb81db77ef9495889 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:08:06 -0700 Subject: [PATCH 36/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7193a226..249f6975 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ try { #### Body Compression -***This feature is NOT JWT specification compliant**, but it can be very useful when you parse your own tokens*. +**This feature is NOT JWT specification compliant**, *but it can be very useful when you parse your own tokens*. If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: From e4e37373b84d61322e83c075a3b657929dc1b8bb Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:12:43 -0700 Subject: [PATCH 37/50] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 249f6975..6da4f317 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ try { If your JWT body is large and you have size restrictions (for example, if embedding a JWT in a URL and the URL must be under a certain length for legacy browsers or mail user agents), you may now compress the JWT body using a `CompressionCodec`: ```java -Jwts.builder().claim("foo", "someReallyDataString...") +Jwts.builder().claim("foo", "someReallyLongDataString...") .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP .signWith(SignatureAlgorithm.HS256, key) .compact(); @@ -165,7 +165,7 @@ Jwts.parser().setSigningKey(key).parseClaimsJws(compact); If the DEFLATE or GZIP algorithms are not sufficient for your needs, you can specify your own Compression algorithms by implementing the `CompressionCodec` interface and setting it on the parser: ```java -Jwts.builder().claim("foo", "someReallyDataString...") +Jwts.builder().claim("foo", "someReallyLongDataString...") .compressWith(new MyCompressionCodec()) .signWith(SignatureAlgorithm.HS256, key) .compact(); @@ -179,7 +179,7 @@ Jwts.parser().setSigningKey(key) .parseClaimsJws(compact); ``` -*NOTE*: Because body compression is not a standard JWT feature, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. It is best used for your own use cases - where you both create and later parse the tokens. This feature will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. +*NOTE*: Because body compression is not a standard JWT feature, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. This feature is best reserved for your own use cases - where you both create and later parse the tokens. It will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. ### 0.5.1 From efe20ee14b08490e287f05ba42e0fc23e9d84427 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:15:06 -0700 Subject: [PATCH 38/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6da4f317..fa8b3db3 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ Jwts.parser().setSigningKey(key) .parseClaimsJws(compact); ``` -*NOTE*: Because body compression is not a standard JWT feature, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. This feature is best reserved for your own use cases - where you both create and later parse the tokens. It will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. +*NOTE*: Because body compression is not JWT specification compliant, you should only enable compression if both your JWT builder and parser are JJWT versions >= 0.6.0, or if you're using another library that implements the exact same functionality. This feature is best reserved for your own use cases - where you both create and later parse the tokens. It will likely cause problems if you compressed a token and expected a 3rd party (who doesn't use JJWT) to parse the token. ### 0.5.1 From 267bc09f6a4e053deff5e3677d669726bb109803 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:19:30 -0700 Subject: [PATCH 39/50] Changing the version from 0.5.2-SNAPSHOT to 0.6.0-SNAPSHOT in preparation for release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 057dea65..23c2ae47 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.5.2-SNAPSHOT + 0.6.0-SNAPSHOT JSON Web Token support for the JVM jar From a4f4da767be3d9fc988a6a2fe961276d28dbdb8c Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:22:44 -0700 Subject: [PATCH 40/50] Update README.md Updated version references to reflect concrete version number per the release --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa8b3db3..e9ef5b20 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Maven: io.jsonwebtoken jjwt - 0.6 + 0.6.0 ``` @@ -24,7 +24,7 @@ Gradle: ```groovy dependencies { - compile 'io.jsonwebtoken:jjwt:0.6' + compile 'io.jsonwebtoken:jjwt:0.6.0' } ``` @@ -99,7 +99,7 @@ These feature sets will be implemented in a future release when possible. Commu ## Release Notes -### 0.6 +### 0.6.0 #### Enforce JWT Claims when Parsing From 98970a7e19de96c45a736a89922b9971d1fbc819 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Oct 2015 16:23:21 -0700 Subject: [PATCH 41/50] Changed version references from 0.5.2 to 0.6.0 (no 0.5.2 release yet). --- src/main/java/io/jsonwebtoken/CompressionCodec.java | 2 +- src/main/java/io/jsonwebtoken/CompressionCodecResolver.java | 2 +- src/main/java/io/jsonwebtoken/CompressionException.java | 2 +- src/main/java/io/jsonwebtoken/Header.java | 4 ++-- src/main/java/io/jsonwebtoken/JwtBuilder.java | 6 +++--- src/main/java/io/jsonwebtoken/JwtParser.java | 6 +++--- .../impl/compression/AbstractCompressionCodec.java | 2 +- .../io/jsonwebtoken/impl/compression/CompressionCodecs.java | 2 +- .../impl/compression/DefaultCompressionCodecResolver.java | 2 +- .../impl/compression/DeflateCompressionCodec.java | 2 +- .../jsonwebtoken/impl/compression/GzipCompressionCodec.java | 2 +- .../impl/compression/AbstractCompressionCodecTest.groovy | 2 +- 12 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/CompressionCodec.java b/src/main/java/io/jsonwebtoken/CompressionCodec.java index 12264f22..b1b3dd6c 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodec.java @@ -20,7 +20,7 @@ package io.jsonwebtoken; * * @see io.jsonwebtoken.impl.compression.DeflateCompressionCodec * @see io.jsonwebtoken.impl.compression.GzipCompressionCodec - * @since 0.5.2 + * @since 0.6.0 */ public interface CompressionCodec { diff --git a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java index 54d3c7fc..afb2e82a 100644 --- a/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/CompressionCodecResolver.java @@ -29,7 +29,7 @@ package io.jsonwebtoken; * {@link io.jsonwebtoken.JwtBuilder#compressWith(CompressionCodec) building} and * {@link io.jsonwebtoken.JwtParser#setCompressionCodecResolver(CompressionCodecResolver) parsing} JWTs.

    * - * @since 0.5.2 + * @since 0.6.0 */ public interface CompressionCodecResolver { diff --git a/src/main/java/io/jsonwebtoken/CompressionException.java b/src/main/java/io/jsonwebtoken/CompressionException.java index d4ac62d3..287ccfb0 100644 --- a/src/main/java/io/jsonwebtoken/CompressionException.java +++ b/src/main/java/io/jsonwebtoken/CompressionException.java @@ -18,7 +18,7 @@ package io.jsonwebtoken; /** * Exception indicating that either compressing or decompressing an JWT body failed. * - * @since 0.5.2 + * @since 0.6.0 */ public class CompressionException extends JwtException { diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java index 81068c73..840d26de 100644 --- a/src/main/java/io/jsonwebtoken/Header.java +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -107,7 +107,7 @@ public interface Header> extends Map { * Returns the JWT calg (Compression Algorithm) header value or {@code null} if not present. * * @return the {@code calg} header parameter value or {@code null} if not present. - * @since 0.5.2 + * @since 0.6.0 */ String getCompressionAlgorithm(); @@ -120,7 +120,7 @@ public interface Header> extends Map { * be able to deserialize a compressed JTW body correctly.

    * * @param calg the JWT compression algorithm {@code calg} value or {@code null} to remove the property from the JSON map. - * @since 0.5.2 + * @since 0.6.0 */ T setCompressionAlgorithm(String calg); diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java index f88d2b0f..df68e9f6 100644 --- a/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -357,15 +357,15 @@ public interface JwtBuilder extends ClaimsMutator { * certain length. Using compression can help ensure the compact JWT fits within that length. However, NOTE:

    * *

    WARNING: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.5.2) are able to consume a compressed JWT body correctly. Only use this method - * if you are sure that you will consume the JWT with JJWT >= 0.5.2 or another library that you know implements + * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. Only use this method + * if you are sure that you will consume the JWT with JJWT >= 0.6.0 or another library that you know implements * the same behavior.

    * * @see io.jsonwebtoken.impl.compression.CompressionCodecs * * @param codec implementation of the {@link CompressionCodec} to be used. * @return the builder for method chaining. - * @since 0.5.2 + * @since 0.6.0 */ JwtBuilder compressWith(CompressionCodec codec); diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 88daf131..11e8a5f6 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -209,8 +209,8 @@ public interface JwtParser { * Sets the {@link CompressionCodecResolver} used to acquire the {@link CompressionCodec} that should be used to * decompress the JWT body. If the parsed JWT is not compressed, this resolver is not used. *

    NOTE: Compression is not defined by the JWT Specification, and it is not expected that other libraries - * (including JJWT versions < 0.5.2) are able to consume a compressed JWT body correctly. This method is only - * useful if the compact JWT was compressed with JJWT >= 0.5.2 or another library that you know implements + * (including JJWT versions < 0.6.0) are able to consume a compressed JWT body correctly. This method is only + * useful if the compact JWT was compressed with JJWT >= 0.6.0 or another library that you know implements * the same behavior.

    *
    Default Support
    *

    JJWT's default {@link JwtParser} implementation supports both the @@ -223,7 +223,7 @@ public interface JwtParser { * * @param compressionCodecResolver the compression codec resolver used to decompress the JWT body. * @return the parser for method chaining. - * @since 0.5.2 + * @since 0.6.0 */ JwtParser setCompressionCodecResolver(CompressionCodecResolver compressionCodecResolver); diff --git a/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java index 9457b68b..e1b2e653 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/AbstractCompressionCodec.java @@ -24,7 +24,7 @@ import java.io.IOException; /** * Abstract class that asserts arguments and wraps IOException with CompressionException. * - * @since 0.5.2 + * @since 0.6.0 */ public abstract class AbstractCompressionCodec implements CompressionCodec { diff --git a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java index f81721cd..19a5f293 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/CompressionCodecs.java @@ -23,7 +23,7 @@ import io.jsonwebtoken.CompressionCodec; * @see #DEFLATE * @see #GZIP * - * @since 0.5.2 + * @since 0.6.0 */ public final class CompressionCodecs { diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java index 159a6f44..fb32c1c6 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DefaultCompressionCodecResolver.java @@ -41,7 +41,7 @@ import io.jsonwebtoken.lang.Strings; * * @see DeflateCompressionCodec * @see GzipCompressionCodec - * @since 0.5.2 + * @since 0.6.0 */ public class DefaultCompressionCodecResolver implements CompressionCodecResolver { diff --git a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java index 90ee9dbc..9f76970b 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/DeflateCompressionCodec.java @@ -26,7 +26,7 @@ import java.util.zip.InflaterOutputStream; /** * Codec implementing the deflate compression algorithm. * - * @since 0.5.2 + * @since 0.6.0 */ public class DeflateCompressionCodec extends AbstractCompressionCodec { diff --git a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java index f15531e1..19bf7e20 100644 --- a/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java +++ b/src/main/java/io/jsonwebtoken/impl/compression/GzipCompressionCodec.java @@ -27,7 +27,7 @@ import java.util.zip.GZIPOutputStream; /** * Codec implementing the gzip compression algorithm. * - * @since 0.5.2 + * @since 0.6.0 */ public class GzipCompressionCodec extends AbstractCompressionCodec implements CompressionCodec { diff --git a/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy index dc497c37..a1e17436 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/compression/AbstractCompressionCodecTest.groovy @@ -20,7 +20,7 @@ import io.jsonwebtoken.CompressionException import org.junit.Test /** - * @since 0.5.2 + * @since 0.6.0 */ class AbstractCompressionCodecTest { static class ExceptionThrowingCodec extends AbstractCompressionCodec { From 8b3f6ab49619de5b164719e660f09a88f3919b93 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 14 Oct 2015 13:50:30 -0700 Subject: [PATCH 42/50] [maven-release-plugin] prepare release 0.6.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 23c2ae47..20e8adb2 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.6.0-SNAPSHOT + 0.6.0 JSON Web Token support for the JVM jar @@ -41,7 +41,7 @@ scm:git:https://github.com/jwtk/jjwt.git scm:git:git@github.com:jwtk/jjwt.git git@github.com:jwtk/jjwt.git - HEAD + 0.6.0 GitHub Issues From 44b652777bca8f28fb117f94c1d6081c11b9fa75 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Wed, 14 Oct 2015 13:50:34 -0700 Subject: [PATCH 43/50] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 20e8adb2..7a4e565b 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.6.0 + 0.7.0-SNAPSHOT JSON Web Token support for the JVM jar @@ -41,7 +41,7 @@ scm:git:https://github.com/jwtk/jjwt.git scm:git:git@github.com:jwtk/jjwt.git git@github.com:jwtk/jjwt.git - 0.6.0 + HEAD GitHub Issues From 687fe6a7371434fd3975c8e7a39ea0beb3881561 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Tue, 27 Oct 2015 21:55:52 -0400 Subject: [PATCH 44/50] Added coveralls coverage report. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e9ef5b20..0808a9f0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/jwtk/jjwt.svg?branch=master)](https://travis-ci.org/jwtk/jjwt) +[![Coverage Status](https://coveralls.io/repos/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/r/jwtk/jjwt?branch=master) # Java JWT: JSON Web Token for Java and Android From 1d9fd734c9ceb3a5106e59286bfda1959a1a4e5c Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Tue, 27 Oct 2015 22:01:36 -0400 Subject: [PATCH 45/50] Added coveralls maven plugin. --- pom.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a4e565b..5e84ff3d 100644 --- a/pom.xml +++ b/pom.xml @@ -274,6 +274,8 @@ cobertura-maven-plugin 2.7 + 256m + true io/jsonwebtoken/lang/*.class @@ -319,6 +321,7 @@ + xml html @@ -364,6 +367,11 @@ + + org.eluder.coveralls + coveralls-maven-plugin + 4.0.0 + @@ -434,4 +442,4 @@ - \ No newline at end of file + From 4773224c744bc461b699dc713e4995e6cf8985b8 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Tue, 27 Oct 2015 22:09:02 -0400 Subject: [PATCH 46/50] Added code to build coverage report to .travis.yml --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 297c5d7c..e7ab1c34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,5 +6,9 @@ jdk: - oraclejdk7 - oraclejdk8 +before_install: + - export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'openjdk7' ] && echo 'true')" install: echo "No need to run mvn install -DskipTests then mvn install. Running mvn install." script: mvn install +after_success: + - test -z "$BUILD_COVERAGE" || mvn clean cobertura:cobertura coveralls:report From 7843179ad5e38ee20d41e307925b54fe880e19e5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Tue, 27 Oct 2015 23:27:18 -0400 Subject: [PATCH 47/50] Improve coverage on compact by exercising JsonProcessingException. --- .../impl/DefaultJwtBuilderTest.groovy | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 4ee7d8e5..ac316704 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.JsonMappingException import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.impl.compression.CompressionCodecs import io.jsonwebtoken.impl.crypto.MacProvider import org.junit.Test @@ -187,6 +188,26 @@ class DefaultJwtBuilderTest { } + @Test + void testCompactCompressionCodecJsonProcessingException() { + def b = new DefaultJwtBuilder() { + @Override + protected byte[] toJson(Object o) throws JsonProcessingException { + if (o instanceof DefaultJwsHeader) { return super.toJson(o) } + throw new JsonProcessingException('simulate json processing exception on claims') + } + } + + def c = Jwts.claims().setSubject("Joe"); + + try { + b.setClaims(c).compressWith(CompressionCodecs.DEFLATE).compact() + fail() + } catch (IllegalArgumentException iae) { + assertEquals iae.message, 'Unable to serialize claims object to json.' + } + } + @Test void testSignWithBytesWithoutHmac() { def bytes = new byte[16]; From 4020dfc1d51395f295b63bc60346839ccd4ff942 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Sat, 21 Nov 2015 15:00:23 -0800 Subject: [PATCH 48/50] Ensures RSA Signatures can work on Android 23 --- .../java/io/jsonwebtoken/impl/crypto/RsaSigner.java | 10 ++++++---- .../io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java index 3a3849df..087ac33f 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java @@ -22,15 +22,17 @@ import java.security.InvalidKeyException; import java.security.Key; import java.security.PrivateKey; import java.security.Signature; -import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAKey; public class RsaSigner extends RsaProvider implements Signer { public RsaSigner(SignatureAlgorithm alg, Key key) { super(alg, key); - if (!(key instanceof RSAPrivateKey)) { - String msg = "RSA signatures must be computed using an RSAPrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSAPrivateKey."; + // https://github.com/jwtk/jjwt/issues/68 + // Instead of checking for an instance of RSAPrivateKey, check for PrivateKey and RSAKey: + if (!(key instanceof PrivateKey && key instanceof RSAKey)) { + String msg = "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + + key.getClass().getName() + " is not an RSA PrivateKey."; throw new IllegalArgumentException(msg); } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy index 39fe04c0..78032b61 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy @@ -58,8 +58,8 @@ class RsaSignerTest { new RsaSigner(SignatureAlgorithm.RS256, key); fail('RsaSigner should reject non RSAPrivateKey instances.') } catch (IllegalArgumentException expected) { - assertEquals expected.message, "RSA signatures must be computed using an RSAPrivateKey. The specified key of type " + - key.getClass().getName() + " is not an RSAPrivateKey."; + assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + + key.getClass().getName() + " is not an RSA PrivateKey."; } } From 35954235769abc09a2a0711acfcb451f42ed1c9b Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Sat, 21 Nov 2015 15:16:42 -0800 Subject: [PATCH 49/50] #68: ensured branch code coverage --- .../impl/crypto/RsaSignerTest.groovy | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy index 78032b61..4b804442 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy @@ -22,6 +22,7 @@ import javax.crypto.spec.SecretKeySpec import java.security.InvalidKeyException import java.security.KeyPair import java.security.KeyPairGenerator +import java.security.MessageDigest import java.security.PrivateKey import java.security.PublicKey @@ -48,13 +49,45 @@ class RsaSignerTest { } @Test - void testConstructorWithoutRsaPrivateKey() { + void testConstructorWithoutPrivateKey() { byte[] bytes = new byte[16] rng.nextBytes(bytes) SecretKeySpec key = new SecretKeySpec(bytes, 'HmacSHA256') try { + //noinspection GroovyResultOfObjectAllocationIgnored + new RsaSigner(SignatureAlgorithm.RS256, key); + fail('RsaSigner should reject non RSAPrivateKey instances.') + } catch (IllegalArgumentException expected) { + assertEquals expected.message, "RSA signatures must be computed using an RSA PrivateKey. The specified key of type " + + key.getClass().getName() + " is not an RSA PrivateKey."; + } + } + + @Test + void testConstructorWithoutRSAKey() { + + //private key, but not an RSAKey instance: + PrivateKey key = new PrivateKey() { + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + + try { + //noinspection GroovyResultOfObjectAllocationIgnored new RsaSigner(SignatureAlgorithm.RS256, key); fail('RsaSigner should reject non RSAPrivateKey instances.') } catch (IllegalArgumentException expected) { @@ -126,4 +159,24 @@ class RsaSignerTest { assertSame se.cause, ex } } + + @Test + void testSignSuccessful() { + + KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); + keyGenerator.initialize(1024); + + KeyPair kp = keyGenerator.genKeyPair(); + PrivateKey privateKey = kp.getPrivate(); + + byte[] bytes = new byte[16] + rng.nextBytes(bytes) + + RsaSigner signer = new RsaSigner(SignatureAlgorithm.RS256, privateKey); + byte[] out1 = signer.sign(bytes) + + byte[] out2 = signer.sign(bytes) + + assertTrue(MessageDigest.isEqual(out1, out2)) + } } From 638d84963f79280fa74ff876def58fc63ed1e609 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Fri, 11 Dec 2015 09:48:50 -0800 Subject: [PATCH 50/50] Updated spec links to final RFC documents --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0808a9f0..b3ea7e01 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM. -JJWT is a 'clean room' implementation based solely on the [JWT](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25), [JWS](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31), [JWE](https://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-31) and [JWA](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-31) RFC draft specifications. +JJWT is a 'clean room' implementation based solely on the [JWT](https://tools.ietf.org/html/rfc7519), [JWS](https://tools.ietf.org/html/rfc7515), [JWE](https://tools.ietf.org/html/rfc7516), [JWK](https://tools.ietf.org/html/rfc7517) and [JWA](https://tools.ietf.org/html/rfc7518) RFC specifications. ## Installation