From 5385e0d7d3671b4736c0246475a03c648b1da68a Mon Sep 17 00:00:00 2001 From: Aaron Davidson Date: Sat, 19 Mar 2016 22:40:44 -0700 Subject: [PATCH 01/36] Avoid potentially critical vulnerability in ECDSA signature validation Quite possible we're missing something here, so please forgive if so. After seeing [this article](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/) (see "RSA or HMAC?" section), we did a quick scan through the JJWT implementation to see if it was vulnerable. While it seems like the RSA check should work, no such check seemed to exist for ECDSA signatures. As a result, it may be possible for users of this library to use `setSigningKey(byte[] key)` while intending to use ECDSA, but have the client alter the algorithm and signature to use HMAC with the public key as the "secret key", allowing the client to inject arbitrary payloads. cc @thomaso-mirodin --- src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index a5d68aec..81e113ce 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -312,6 +312,9 @@ public class DefaultJwtParser implements JwtParser { Assert.isTrue(!algorithm.isRsa(), "Key bytes cannot be specified for RSA signatures. Please specify a PublicKey or PrivateKey instance."); + Assert.isTrue(!algorithm.isEllipticCurve(), + "Key bytes cannot be specified for ECDSA signatures. Please specify a PublicKey instance."); + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } } From 707f7bc046b363ac6262f43bdc0885ff04f35f1c Mon Sep 17 00:00:00 2001 From: Aaron Davidson Date: Sat, 26 Mar 2016 12:17:26 -0700 Subject: [PATCH 02/36] Change assert to require hmac --- src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 81e113ce..3a945454 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -309,11 +309,8 @@ public class DefaultJwtParser implements JwtParser { if (!Objects.isEmpty(keyBytes)) { - Assert.isTrue(!algorithm.isRsa(), - "Key bytes cannot be specified for RSA signatures. Please specify a PublicKey or PrivateKey instance."); - - Assert.isTrue(!algorithm.isEllipticCurve(), - "Key bytes cannot be specified for ECDSA signatures. Please specify a PublicKey instance."); + Assert.isTrue(algorithm.isHmac(), + "Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance."); key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); } From 39ee58a5115ba8364f2617a927adc16dbfa86e3a Mon Sep 17 00:00:00 2001 From: Brian Matzon Date: Wed, 27 Apr 2016 12:15:36 +0200 Subject: [PATCH 03/36] implement hashCode and equals in JwtMap --- .../java/io/jsonwebtoken/impl/JwtMap.java | 12 ++++++ .../io.jsonwebtoken.impl/JavaJwtMapTest.java | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 40d21224..328c4c4f 100644 --- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -155,4 +155,16 @@ public class JwtMap implements Map { public String toString() { return map.toString(); } + + @Override + public int hashCode() + { + return map.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + return map.equals(obj); + } } diff --git a/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java b/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java new file mode 100644 index 00000000..2b1aa56b --- /dev/null +++ b/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java @@ -0,0 +1,39 @@ +package io.jsonwebtoken.impl; + +import org.junit.Test; + +import static junit.framework.Assert.assertTrue; +import static junit.framework.TestCase.assertEquals; + +/** + * Java specific test to ensure we don't hit Groovy's DefaultGroovyMethods + */ +public class JavaJwtMapTest +{ + + @Test + public void testEquals() throws Exception + { + JwtMap jwtMap1 = new JwtMap(); + jwtMap1.put("a", "a"); + + JwtMap jwtMap2 = new JwtMap(); + jwtMap2.put("a", "a"); + + assertEquals(jwtMap1, jwtMap2); + } + + @Test + public void testHashCode() throws Exception + { + JwtMap jwtMap = new JwtMap(); + int hashCodeEmpty = jwtMap.hashCode(); + + jwtMap.put("a", "b"); + int hashCodeNonEmpty = jwtMap.hashCode(); + assertTrue(hashCodeEmpty != hashCodeNonEmpty); + + int identityHash = System.identityHashCode(jwtMap); + assertTrue(hashCodeNonEmpty != identityHash); + } +} From 4be4912cb24e0effe6f2f49b25f4e439b4f24b6d Mon Sep 17 00:00:00 2001 From: Brian Matzon Date: Mon, 6 Jun 2016 23:43:52 +0200 Subject: [PATCH 04/36] moved Java test into groovy --- .../io/jsonwebtoken/impl/JwtMapTest.groovy | 24 ++++++++++++ .../io.jsonwebtoken.impl/JavaJwtMapTest.java | 39 ------------------- 2 files changed, 24 insertions(+), 39 deletions(-) delete mode 100644 src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java diff --git a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy index 0d00f6d7..446eeddd 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy @@ -124,4 +124,28 @@ class JwtMapTest { def s = ['b', 'd'] assertTrue m.values().containsAll(s) && s.containsAll(m.values()) } + + @Test + public void testEquals() throws Exception { + def m1 = new JwtMap(); + m1.put("a", "a"); + + def m2 = new JwtMap(); + m2.put("a", "a"); + + assertEquals(m1, m2); + } + + @Test + public void testHashcode() throws Exception { + def m = new JwtMap(); + def hashCodeEmpty = m.hashCode(); + + m.put("a", "b"); + def hashCodeNonEmpty = m.hashCode(); + assertTrue(hashCodeEmpty != hashCodeNonEmpty); + + def identityHash = System.identityHashCode(m); + assertTrue(hashCodeNonEmpty != identityHash); + } } diff --git a/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java b/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java deleted file mode 100644 index 2b1aa56b..00000000 --- a/src/test/java/io.jsonwebtoken.impl/JavaJwtMapTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.jsonwebtoken.impl; - -import org.junit.Test; - -import static junit.framework.Assert.assertTrue; -import static junit.framework.TestCase.assertEquals; - -/** - * Java specific test to ensure we don't hit Groovy's DefaultGroovyMethods - */ -public class JavaJwtMapTest -{ - - @Test - public void testEquals() throws Exception - { - JwtMap jwtMap1 = new JwtMap(); - jwtMap1.put("a", "a"); - - JwtMap jwtMap2 = new JwtMap(); - jwtMap2.put("a", "a"); - - assertEquals(jwtMap1, jwtMap2); - } - - @Test - public void testHashCode() throws Exception - { - JwtMap jwtMap = new JwtMap(); - int hashCodeEmpty = jwtMap.hashCode(); - - jwtMap.put("a", "b"); - int hashCodeNonEmpty = jwtMap.hashCode(); - assertTrue(hashCodeEmpty != hashCodeNonEmpty); - - int identityHash = System.identityHashCode(jwtMap); - assertTrue(hashCodeNonEmpty != identityHash); - } -} From f08386c63b9da6c3765cabe2050be7cc1b1797b8 Mon Sep 17 00:00:00 2001 From: Brian Matzon Date: Wed, 8 Jun 2016 00:20:23 +0200 Subject: [PATCH 05/36] formatting --- src/main/java/io/jsonwebtoken/impl/JwtMap.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java index 328c4c4f..ba702d7c 100644 --- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -157,14 +157,12 @@ public class JwtMap implements Map { } @Override - public int hashCode() - { + public int hashCode() { return map.hashCode(); } @Override - public boolean equals(Object obj) - { + public boolean equals(Object obj) { return map.equals(obj); } } From 26a14fd3c3ad4e3242e424a177f7654e0db0e472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Kj=C3=A4ll?= Date: Mon, 13 Jun 2016 14:40:35 +0200 Subject: [PATCH 06/36] javadoc typo Updated the number of bits for the HS512 algorithm in the javadoc comment. --- src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 7ea2ef9c..1ce280bb 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -69,7 +69,7 @@ public abstract class MacProvider extends SignatureProvider { * * * - *
Signature Algorithm Generated Key Size
HS256 256 bits (32 bytes)
HS384 384 bits (48 bytes)
HS512 256 bits (64 bytes)
+ * HS512 512 bits (64 bytes) * * @param alg the signature algorithm that will be used with the generated key * @param random the secure random number generator used during key generation From a73e0044b8b2a2214714e183f3a1ed35e7430b73 Mon Sep 17 00:00:00 2001 From: Martin Treurnicht Date: Mon, 27 Jun 2016 15:43:35 -0700 Subject: [PATCH 07/36] Fixed ECDSA Signing and verification to use R + S curve points as per spec https://tools.ietf.org/html/rfc7515#page-45 --- pom.xml | 26 ++- .../io/jsonwebtoken/impl/crypto/ECDSA.java | 177 ++++++++++++++++++ .../EllipticCurveSignatureValidator.java | 11 +- .../impl/crypto/EllipticCurveSigner.java | 13 +- .../io/jsonwebtoken/lang/JOSEException.java | 22 +++ ...EllipticCurveSignatureValidatorTest.groovy | 32 +++- .../crypto/EllipticCurveSignerTest.groovy | 30 +++ 7 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java create mode 100644 src/main/java/io/jsonwebtoken/lang/JOSEException.java diff --git a/pom.xml b/pom.xml index bd0dec0e..585e4700 100644 --- a/pom.xml +++ b/pom.xml @@ -307,12 +307,13 @@ 100 90 + + io.jsonwebtoken.impl.crypto.ECDSA + 80 + 60 + - - xml - html - @@ -374,7 +375,6 @@ - jdk8 @@ -442,4 +442,20 @@ + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + xml + html + + + + + + diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java b/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java new file mode 100644 index 00000000..d7d6a76e --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java @@ -0,0 +1,177 @@ +package io.jsonwebtoken.impl.crypto; + + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.JOSEException; + + +/** + * Elliptic Curve Digital Signature Algorithm (ECDSA) functions and utilities. + */ +public class ECDSA { + + + /** + * Returns the expected signature byte array length (R + S parts) for + * the specified ECDSA algorithm. + * + * @param alg The ECDSA algorithm. Must be supported and not + * {@code null}. + * + * @return The expected byte array length for the signature. + * + * @throws JOSEException If the algorithm is not supported. + */ + public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) + throws JOSEException { + + switch (alg) { + case ES256: return 64; + case ES384: return 96; + case ES512: return 132; + default: + throw new JOSEException("unsupported algorithm: " + alg.name()); + } + } + + + /** + * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated + * R + S format expected by ECDSA JWS. + * + * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. + * @param outputLength The expected length of the ECDSA JWS signature. + * + * @return The ECDSA JWS encoded signature. + * + * @throws JOSEException If the ASN.1/DER signature format is invalid. + */ + public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) + throws JOSEException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JOSEException("Invalid ECDSA signature format"); + } + + int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JOSEException("Invalid ECDSA signature format"); + } + + byte rLength = derSignature[offset + 1]; + + int i; + for (i = rLength; (i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0); i--) { + // do nothing + } + + byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j; + for (j = sLength; (j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0); j--) { + // do nothing + } + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JOSEException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + + + /** + * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by + * the JCA verifier. + * + * @param jwsSignature The JWS signature, consisting of the + * concatenated R and S values. Must not be + * {@code null}. + * + * @return The ASN.1/DER encoded signature. + * + * @throws JOSEException If the ECDSA JWS signature format is invalid. + */ + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) + throws JOSEException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while((i > 0) && (jwsSignature[rawLen - i] == 0)) i--; + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) k--; + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JOSEException("Invalid ECDSA signature format"); + } + + int offset; + + final byte derSignature[]; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } + + + /** + * Prevents public instantiation. + */ + private ECDSA() {} +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 62097184..172521af 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; - import java.security.InvalidKeyException; import java.security.Key; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; + public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { private static final String EC_PUBLIC_KEY_REQD_MSG = @@ -40,7 +40,8 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple Signature sig = createSignatureInstance(); PublicKey publicKey = (PublicKey) key; try { - return doVerify(sig, publicKey, data, signature); + byte[] derSignature = ECDSA.transcodeSignatureToDER(signature); + return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); throw new SignatureException(msg, e); diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java index 4c80268f..41ce6c9b 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -15,15 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; - import java.security.InvalidKeyException; import java.security.Key; import java.security.PrivateKey; import java.security.Signature; import java.security.interfaces.ECPrivateKey; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.JOSEException; + public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { @@ -43,14 +44,16 @@ public class EllipticCurveSigner extends EllipticCurveProvider implements Signer throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e); } catch (java.security.SignatureException e) { throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e); + } catch (JOSEException e) { + throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e); } } - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException { PrivateKey privateKey = (PrivateKey)key; Signature sig = createSignatureInstance(); sig.initSign(privateKey); sig.update(data); - return sig.sign(); + return ECDSA.transcodeSignatureToConcat(sig.sign(), ECDSA.getSignatureByteArrayLength(alg)); } } diff --git a/src/main/java/io/jsonwebtoken/lang/JOSEException.java b/src/main/java/io/jsonwebtoken/lang/JOSEException.java new file mode 100644 index 00000000..5b611909 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/JOSEException.java @@ -0,0 +1,22 @@ +package io.jsonwebtoken.lang; + + +/** + * Javascript Object Signing and Encryption (JOSE) exception. + */ +public class JOSEException extends Exception { + + + private static final long serialVersionUID = 1L; + + + /** + * Creates a new JOSE exception with the specified message. + * + * @param message The exception message. + */ + public JOSEException(final String message) { + + super(message); + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index e595bd89..6a7de843 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -17,16 +17,20 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException - -import java.security.InvalidKeyException -import java.security.PublicKey -import java.security.Signature - +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test + +import java.security.* +import java.security.spec.X509EncodedKeySpec + import static org.junit.Assert.* class EllipticCurveSignatureValidatorTest { + static { + Security.addProvider(new BouncyCastleProvider()) + } + @Test void testDoVerifyWithInvalidKeyException() { @@ -53,4 +57,22 @@ class EllipticCurveSignatureValidatorTest { assertSame se.cause, ex } } + + @Test + void ecdsaSignatureComplianceTest() { + def fact = KeyFactory.getInstance("ECDSA", "BC"); + def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" + def pub = fact.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))) + def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) + def verifier = { token -> + def signatureStart = token.lastIndexOf('.') + def withoutSignature = token.substring(0, signatureStart) + def signature = token.substring(signatureStart + 1) + assert v.isValid(withoutSignature.getBytes("US-ASCII"), Base64.getUrlDecoder().decode(signature)), "Signature do not match that of other implementations" + } + //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 + verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") + //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 + verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy index 4587f1f6..c353a7dd 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.lang.JOSEException import java.security.InvalidKeyException import java.security.KeyPair @@ -79,6 +80,35 @@ class EllipticCurveSignerTest { } } + @Test + void testDoSignWithJoseSignatureFormatException() { + + KeyPair kp = EllipticCurveProvider.generateKeyPair() + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + String msg = 'foo' + final JOSEException ex = new JOSEException(msg) + + def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { + @Override + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException { + throw ex + } + } + + byte[] bytes = new byte[16] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) + + try { + signer.sign(bytes) + fail(); + } catch (SignatureException se) { + assertEquals se.message, 'Unable to convert signature to JOSE format. ' + msg + assertSame se.cause, ex + } + } + @Test void testDoSignWithJdkSignatureException() { From c60deebb644148b3e131d706ecbcf3a5d4048532 Mon Sep 17 00:00:00 2001 From: Martin Treurnicht Date: Mon, 27 Jun 2016 16:02:06 -0700 Subject: [PATCH 08/36] Removed java 8 dependencies in test --- .../impl/crypto/EllipticCurveSignatureValidatorTest.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index 6a7de843..bc6dea44 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.impl.TextCodec import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test @@ -62,13 +63,13 @@ class EllipticCurveSignatureValidatorTest { void ecdsaSignatureComplianceTest() { def fact = KeyFactory.getInstance("ECDSA", "BC"); def publicKey = "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQASisgweVL1tAtIvfmpoqvdXF8sPKTV9YTKNxBwkdkm+/auh4pR8TbaIfsEzcsGUVv61DFNFXb0ozJfurQ59G2XcgAn3vROlSSnpbIvuhKrzL5jwWDTaYa5tVF1Zjwia/5HUhKBkcPuWGXg05nMjWhZfCuEetzMLoGcHmtvabugFrqsAg=" - def pub = fact.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))) + def pub = fact.generatePublic(new X509EncodedKeySpec(TextCodec.BASE64.decode(publicKey))) def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, pub) def verifier = { token -> def signatureStart = token.lastIndexOf('.') def withoutSignature = token.substring(0, signatureStart) def signature = token.substring(signatureStart + 1) - assert v.isValid(withoutSignature.getBytes("US-ASCII"), Base64.getUrlDecoder().decode(signature)), "Signature do not match that of other implementations" + assert v.isValid(withoutSignature.getBytes("US-ASCII"), TextCodec.BASE64URL.decode(signature)), "Signature do not match that of other implementations" } //Test verification for token created using https://github.com/auth0/node-jsonwebtoken/tree/v7.0.1 verifier("eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30.Aab4x7HNRzetjgZ88AMGdYV2Ml7kzFbl8Ql2zXvBores7iRqm2nK6810ANpVo5okhHa82MQf2Q_Zn4tFyLDR9z4GAcKFdcAtopxq1h8X58qBWgNOc0Bn40SsgUc8wOX4rFohUCzEtnUREePsvc9EfXjjAH78WD2nq4tn-N94vf14SncQ") From 61510dfca58dd40b4b32c708935126785dcff48c Mon Sep 17 00:00:00 2001 From: Martin Treurnicht Date: Tue, 28 Jun 2016 12:12:40 -0700 Subject: [PATCH 09/36] Cleanup as per request of https://github.com/lhazlewood --- pom.xml | 5 - .../io/jsonwebtoken/impl/crypto/ECDSA.java | 177 ------------------ .../impl/crypto/EllipticCurveProvider.java | 169 ++++++++++++++++- .../EllipticCurveSignatureValidator.java | 4 +- .../impl/crypto/EllipticCurveSigner.java | 8 +- .../impl/crypto/SignatureProvider.java | 10 +- .../io/jsonwebtoken/lang/JOSEException.java | 22 --- ...EllipticCurveSignatureValidatorTest.groovy | 110 +++++++++++ .../crypto/EllipticCurveSignerTest.groovy | 8 +- 9 files changed, 292 insertions(+), 221 deletions(-) delete mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java delete mode 100644 src/main/java/io/jsonwebtoken/lang/JOSEException.java diff --git a/pom.xml b/pom.xml index 585e4700..0d11dbac 100644 --- a/pom.xml +++ b/pom.xml @@ -307,11 +307,6 @@ 100 90 - - io.jsonwebtoken.impl.crypto.ECDSA - 80 - 60 - diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java b/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java deleted file mode 100644 index d7d6a76e..00000000 --- a/src/main/java/io/jsonwebtoken/impl/crypto/ECDSA.java +++ /dev/null @@ -1,177 +0,0 @@ -package io.jsonwebtoken.impl.crypto; - - -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.JOSEException; - - -/** - * Elliptic Curve Digital Signature Algorithm (ECDSA) functions and utilities. - */ -public class ECDSA { - - - /** - * Returns the expected signature byte array length (R + S parts) for - * the specified ECDSA algorithm. - * - * @param alg The ECDSA algorithm. Must be supported and not - * {@code null}. - * - * @return The expected byte array length for the signature. - * - * @throws JOSEException If the algorithm is not supported. - */ - public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) - throws JOSEException { - - switch (alg) { - case ES256: return 64; - case ES384: return 96; - case ES512: return 132; - default: - throw new JOSEException("unsupported algorithm: " + alg.name()); - } - } - - - /** - * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated - * R + S format expected by ECDSA JWS. - * - * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. - * @param outputLength The expected length of the ECDSA JWS signature. - * - * @return The ECDSA JWS encoded signature. - * - * @throws JOSEException If the ASN.1/DER signature format is invalid. - */ - public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) - throws JOSEException { - - if (derSignature.length < 8 || derSignature[0] != 48) { - throw new JOSEException("Invalid ECDSA signature format"); - } - - int offset; - if (derSignature[1] > 0) { - offset = 2; - } else if (derSignature[1] == (byte) 0x81) { - offset = 3; - } else { - throw new JOSEException("Invalid ECDSA signature format"); - } - - byte rLength = derSignature[offset + 1]; - - int i; - for (i = rLength; (i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0); i--) { - // do nothing - } - - byte sLength = derSignature[offset + 2 + rLength + 1]; - - int j; - for (j = sLength; (j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0); j--) { - // do nothing - } - - int rawLen = Math.max(i, j); - rawLen = Math.max(rawLen, outputLength / 2); - - if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset - || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength - || derSignature[offset] != 2 - || derSignature[offset + 2 + rLength] != 2) { - throw new JOSEException("Invalid ECDSA signature format"); - } - - final byte[] concatSignature = new byte[2 * rawLen]; - - System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); - System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); - - return concatSignature; - } - - - - /** - * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by - * the JCA verifier. - * - * @param jwsSignature The JWS signature, consisting of the - * concatenated R and S values. Must not be - * {@code null}. - * - * @return The ASN.1/DER encoded signature. - * - * @throws JOSEException If the ECDSA JWS signature format is invalid. - */ - public static byte[] transcodeSignatureToDER(byte[] jwsSignature) - throws JOSEException { - - int rawLen = jwsSignature.length / 2; - - int i = rawLen; - - while((i > 0) && (jwsSignature[rawLen - i] == 0)) i--; - - int j = i; - - if (jwsSignature[rawLen - i] < 0) { - j += 1; - } - - int k = rawLen; - - while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) k--; - - int l = k; - - if (jwsSignature[2 * rawLen - k] < 0) { - l += 1; - } - - int len = 2 + j + 2 + l; - - if (len > 255) { - throw new JOSEException("Invalid ECDSA signature format"); - } - - int offset; - - final byte derSignature[]; - - if (len < 128) { - derSignature = new byte[2 + 2 + j + 2 + l]; - offset = 1; - } else { - derSignature = new byte[3 + 2 + j + 2 + l]; - derSignature[1] = (byte) 0x81; - offset = 2; - } - - derSignature[0] = 48; - derSignature[offset++] = (byte) len; - derSignature[offset++] = 2; - derSignature[offset++] = (byte) j; - - System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); - - offset += j; - - derSignature[offset++] = 2; - derSignature[offset++] = (byte) l; - - System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); - - return derSignature; - } - - - /** - * Prevents public instantiation. - */ - private ECDSA() {} -} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java index 1c215e22..f388c8d9 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java @@ -15,9 +15,6 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,6 +22,10 @@ import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + /** * ElliptiCurve crypto provider. * @@ -135,4 +136,166 @@ public abstract class EllipticCurveProvider extends SignatureProvider { throw new IllegalStateException("Unable to generate Elliptic Curve KeyPair: " + e.getMessage(), e); } } + + /** + * Returns the expected signature byte array length (R + S parts) for + * the specified ECDSA algorithm. + * + * @param alg The ECDSA algorithm. Must be supported and not + * {@code null}. + * + * @return The expected byte array length for the signature. + * + * @throws JwtException If the algorithm is not supported. + */ + public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) + throws JwtException { + + switch (alg) { + case ES256: return 64; + case ES384: return 96; + case ES512: return 132; + default: + throw new JwtException("Unsupported Algorithm: " + alg.name()); + } + } + + + /** + * Transcodes the JCA ASN.1/DER-encoded signature into the concatenated + * R + S format expected by ECDSA JWS. + * + * @param derSignature The ASN1./DER-encoded. Must not be {@code null}. + * @param outputLength The expected length of the ECDSA JWS signature. + * + * @return The ECDSA JWS encoded signature. + * + * @throws JwtException If the ASN.1/DER signature format is invalid. + */ + public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) + throws JwtException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JwtException("Invalid ECDSA signature format"); + } + + byte rLength = derSignature[offset + 1]; + + int i = rLength; + while ((i > 0) + && (derSignature[(offset + 2 + rLength) - i] == 0)) + i--; + + byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j = sLength; + while ((j > 0) + && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) + j--; + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JwtException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + + + /** + * Transcodes the ECDSA JWS signature into ASN.1/DER format for use by + * the JCA verifier. + * + * @param jwsSignature The JWS signature, consisting of the + * concatenated R and S values. Must not be + * {@code null}. + * + * @return The ASN.1/DER encoded signature. + * + * @throws JwtException If the ECDSA JWS signature format is invalid. + */ + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) + throws JwtException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while((i > 0) + && (jwsSignature[rawLen - i] == 0)) + i--; + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) + && (jwsSignature[2 * rawLen - k] == 0)) + k--; + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JwtException("Invalid ECDSA signature format"); + } + + int offset; + + final byte derSignature[]; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 172521af..7d9435ec 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -40,7 +40,9 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple Signature sig = createSignatureInstance(); PublicKey publicKey = (PublicKey) key; try { - byte[] derSignature = ECDSA.transcodeSignatureToDER(signature); + int expectedSize = getSignatureByteArrayLength(alg); + //if the expected size is not valid for JOSE, fall back to ASN.1 DER signature + byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. " + e.getMessage(); diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java index 41ce6c9b..14913f18 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -21,9 +21,9 @@ import java.security.PrivateKey; import java.security.Signature; import java.security.interfaces.ECPrivateKey; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.JOSEException; public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { @@ -44,16 +44,16 @@ public class EllipticCurveSigner extends EllipticCurveProvider implements Signer throw new SignatureException("Invalid Elliptic Curve PrivateKey. " + e.getMessage(), e); } catch (java.security.SignatureException e) { throw new SignatureException("Unable to calculate signature using Elliptic Curve PrivateKey. " + e.getMessage(), e); - } catch (JOSEException e) { + } catch (JwtException e) { throw new SignatureException("Unable to convert signature to JOSE format. " + e.getMessage(), e); } } - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException { + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { PrivateKey privateKey = (PrivateKey)key; Signature sig = createSignatureInstance(); sig.initSign(privateKey); sig.update(data); - return ECDSA.transcodeSignatureToConcat(sig.sign(), ECDSA.getSignatureByteArrayLength(alg)); + return transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); } } diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java index 4fb4f8a0..e10c7a77 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; - import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Signature; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; + abstract class SignatureProvider { /** diff --git a/src/main/java/io/jsonwebtoken/lang/JOSEException.java b/src/main/java/io/jsonwebtoken/lang/JOSEException.java deleted file mode 100644 index 5b611909..00000000 --- a/src/main/java/io/jsonwebtoken/lang/JOSEException.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.jsonwebtoken.lang; - - -/** - * Javascript Object Signing and Encryption (JOSE) exception. - */ -public class JOSEException extends Exception { - - - private static final long serialVersionUID = 1L; - - - /** - * Creates a new JOSE exception with the specified message. - * - * @param message The exception message. - */ - public JOSEException(final String message) { - - super(message); - } -} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index bc6dea44..364b1c57 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -15,6 +15,7 @@ */ package io.jsonwebtoken.impl.crypto +import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException import io.jsonwebtoken.impl.TextCodec @@ -76,4 +77,113 @@ class EllipticCurveSignatureValidatorTest { //Test verification for token created using https://github.com/jwt/ruby-jwt/tree/v1.5.4 verifier("eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzUxMiJ9.eyJ0ZXN0IjoidGVzdCJ9.AV26tERbSEwcoDGshneZmhokg-tAKUk0uQBoHBohveEd51D5f6EIs6cskkgwtfzs4qAGfx2rYxqQXr7LTXCNquKiAJNkTIKVddbPfped3_TQtmHZTmMNiqmWjiFj7Y9eTPMMRRu26w4gD1a8EQcBF-7UGgeH4L_1CwHJWAXGbtu7uMUn") } + + @Test + void legacySignatureCompatTest() { + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair() + def signature = Signature.getInstance(SignatureAlgorithm.ES512.jcaName, "BC") + def data = withoutSignature.getBytes("US-ASCII") + signature.initSign(keypair.private) + signature.update(data) + def signed = signature.sign() + assert new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, keypair.public).isValid(data, signed) + } + + @Test + void invalidAlgorithmTest() { + def invalidAlgorithm = SignatureAlgorithm.HS256 + try { + EllipticCurveProvider.getSignatureByteArrayLength(invalidAlgorithm) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Unsupported Algorithm: ' + invalidAlgorithm.name() + } + } + + @Test + void invalidECDSASignatureFormatTest() { + try { + def signature = new byte[257] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + EllipticCurveProvider.transcodeSignatureToDER(signature) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void invalidDERSignatureToJoseFormatTest() { + def verify = { signature -> + try { + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + def signature = new byte[257] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + //invalid type + signature[0] = 34 + verify(signature) + def shortSignature = new byte[7] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(shortSignature) + verify(shortSignature) + signature[0] = 48 +// signature[1] = 0x81 + signature[1] = -10 + verify(signature) + } + + @Test + void edgeCaseSignatureLengthTest() { + def signature = new byte[1] + EllipticCurveProvider.transcodeSignatureToDER(signature) + } + + @Test + void edgeCaseSignatureToConcatLengthTest() { + try { + def signature = TextCodec.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureTest() { + try { + def signature = TextCodec.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { + try { + def signature = TextCodec.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } + + @Test + void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { + try { + def signature = TextCodec.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + fail() + } catch (JwtException e) { + assertEquals e.message, 'Invalid ECDSA signature format' + } + } } diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy index c353a7dd..02067309 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto +import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.SignatureException -import io.jsonwebtoken.lang.JOSEException +import org.junit.Test import java.security.InvalidKeyException import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import org.junit.Test import static org.junit.Assert.* class EllipticCurveSignerTest { @@ -88,11 +88,11 @@ class EllipticCurveSignerTest { PrivateKey privateKey = kp.getPrivate(); String msg = 'foo' - final JOSEException ex = new JOSEException(msg) + final JwtException ex = new JwtException(msg) def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { @Override - protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JOSEException { + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { throw ex } } From 174e1b13b882f07853ca60578a9315a1e1d5daf1 Mon Sep 17 00:00:00 2001 From: Martin Treurnicht Date: Tue, 28 Jun 2016 12:19:54 -0700 Subject: [PATCH 10/36] Add back swarm test for 100% coverage --- .../EllipticCurveSignatureValidatorTest.groovy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index 364b1c57..f23c3852 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -186,4 +186,19 @@ class EllipticCurveSignatureValidatorTest { assertEquals e.message, 'Invalid ECDSA signature format' } } + + @Test + void verifySwarmTest() { + for (SignatureAlgorithm algorithm : [SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512]) { + int i = 0 + while(i < 10) { + i++ + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair() + def data = withoutSignature.getBytes("US-ASCII") + def signature = new EllipticCurveSigner(algorithm, keypair.private).sign(data) + assert new EllipticCurveSignatureValidator(algorithm, keypair.public).isValid(data, signature) + } + } + } } From c3e5f952425b98bcf5147045ace0976cfc3e3def Mon Sep 17 00:00:00 2001 From: Martin Treurnicht Date: Thu, 30 Jun 2016 13:46:07 -0700 Subject: [PATCH 11/36] Added more descriptive backwards compatibility information --- .../impl/crypto/EllipticCurveSignatureValidator.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index 7d9435ec..09ab14db 100644 --- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -41,7 +41,13 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple PublicKey publicKey = (PublicKey) key; try { int expectedSize = getSignatureByteArrayLength(alg); - //if the expected size is not valid for JOSE, fall back to ASN.1 DER signature + /** + * + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature. + * This fallback is for backwards compatibility ONLY (to support tokens generated by previous versions of jjwt) + * and backwards compatibility will possibly be removed in a future version of this library. + * + * **/ byte[] derSignature = expectedSize != signature.length && signature[0] == 0x30 ? signature : EllipticCurveProvider.transcodeSignatureToDER(signature); return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { From 08992610740f333fa4d12a66d980e9c52380df91 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Fri, 17 Jun 2016 14:30:47 -0700 Subject: [PATCH 12/36] Separated CHANGELOG from README --- CHANGELOG.md | 257 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 258 +-------------------------------------------------- 2 files changed, 258 insertions(+), 257 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c7a33cc1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,257 @@ +## Release Notes + +### 0.6.0 + +#### Enforce JWT Claims when Parsing + +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: + +```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(fieldName, requiredFieldValue)` 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) + +#### 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", "someReallyLongDataString...") + .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", "someReallyLongDataString...") + .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 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 + +- 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. + +### 0.5 + +- Android support! Android's built-in Base64 codec will be used if JJWT detects it is running in an Android environment. Other than Base64, all other parts of JJWT were already Android-compliant. Now it is fully compliant. + +- Elliptic Curve signature algorithms! `SignatureAlgorithm.ES256`, `ES384` and `ES512` are now supported. + +- Super convenient key generation methods, so you don't have to worry how to do this safely: + - `MacProvider.generateKey(); //or generateKey(SignatureAlgorithm)` + - `RsaProvider.generateKeyPair(); //or generateKeyPair(sizeInBits)` + - `EllipticCurveProvider.generateKeyPair(); //or generateKeyPair(SignatureAlgorithm)` + + The `generate`* methods that accept an `SignatureAlgorithm` argument know to generate a key of sufficient strength that reflects the specified algorithm strength. + +Please see the full [0.5 closed issues list](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5+is%3Aclosed) for more information. + +### 0.4 + +- [Issue 8](https://github.com/jwtk/jjwt/issues/8): Add ability to find signing key by inspecting the JWS values before verifying the signature. + +This is a handy little feature. If you need to parse a signed JWT (a JWS) and you don't know which signing key was used to sign it, you can now use the new `SigningKeyResolver` concept. + +A `SigningKeyresolver` can inspect the JWS header and body (Claims or String) _before_ the JWS signature is verified. By inspecting the data, you can find the key and return it, and the parser will use the returned key to validate the signature. For example: + +```java +SigningKeyResolver resolver = new MySigningKeyResolver(); + +Jws jws = Jwts.parser().setSigningKeyResolver(resolver).parseClaimsJws(compact); +``` + +The signature is still validated, and the JWT instance will still not be returned if the jwt string is invalid, as expected. You just get to 'see' the JWT data for key discovery before the parser validates. Nice. + +This of course requires that you put some sort of information in the JWS when you create it so that your `SigningKeyResolver` implementation can look at it later and look up the key. The *standard* way to do this is to use the JWS `kid` ('key id') field, for example: + +```java +Jwts.builder().setHeaderParam("kid", your_signing_key_id_NOT_THE_SECRET).build(); +``` + +You could of course set any other header parameter or claims parameter instead of setting `kid` if you want - that's just the default field reserved for signing key identification. If you can locate the signing key based on other information in the header or claims, you don't need to set the `kid` field - just make sure your resolver implementation knows how to look up the key. + +Finally, a nice `SigningKeyResolverAdapter` is provided to allow you to write quick and simple subclasses or anonymous classes instead of having to implement the `SigningKeyResolver` interface directly. For example: + +```java +Jws jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { + @Override + public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { + //inspect the header or claims, lookup and return the signing key + String keyId = header.getKeyId(); //or any other field that you need to inspect + return getSigningKey(keyId); //implement me + }}) + .parseClaimsJws(compact); +``` + +### 0.3 + +- [Issue 6](https://github.com/jwtk/jjwt/issues/6): Parsing an expired Claims JWT or JWS (as determined by the `exp` claims field) will now throw an `ExpiredJwtException`. +- [Issue 7](https://github.com/jwtk/jjwt/issues/7): Parsing a premature Claims JWT or JWS (as determined by the `nbf` claims field) will now throw a `PrematureJwtException`. + +### 0.2 + +#### More convenient Claims building + +This release adds convenience methods to the `JwtBuilder` interface so you can set claims directly on the builder without having to create a separate Claims instance/builder, reducing the amount of code you have to write. For example, this: + +```java +Claims claims = Jwts.claims().setSubject("Joe"); + +String compactJwt = Jwts.builder().setClaims(claims).signWith(HS256, key).compact(); +``` + +can now be written as: + +```java +String compactJwt = Jwts.builder().setSubject("Joe").signWith(HS256, key).compact(); +``` + +A Claims instance based on the specified claims will be created and set as the JWT's payload automatically. + +#### Type-safe handling for JWT and JWS with generics + +The following < 0.2 code produced a JWT as expected: + +```java +Jwt jwt = Jwts.parser().setSigningKey(key).parse(compact); +``` + +But you couldn't easily determine if the `jwt` was a `JWT` or `JWS` instance or if the body was a `Claims` instance or a plaintext `String` without resorting to a bunch of yucky `instanceof` checks. In 0.2, we introduce the `JwtHandler` when you don't know the exact format of the compact JWT string ahead of time, and parsing convenience methods when you do. + +##### JwtHandler + +If you do not know the format of the compact JWT string at the time you try to parse it, you can determine what type it is after parsing by providing a `JwtHandler` instance to the `JwtParser` with the new `parse(String compactJwt, JwtHandler handler)` method. For example: + +```java +T returnVal = Jwts.parser().setSigningKey(key).parse(compact, new JwtHandler() { + @Override + public T onPlaintextJwt(Jwt jwt) { + //the JWT parsed was an unsigned plaintext JWT + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJwt(Jwt jwt) { + //the JWT parsed was an unsigned Claims JWT + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onPlaintextJws(Jws jws) { + //the JWT parsed was a signed plaintext JWS + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJws(Jws jws) { + //the JWT parsed was a signed Claims JWS + //inspect it, then return an instance of T (see returnVal above) + } +}); +``` + +Of course, if you know you'll only have to parse a subset of the above, you can use the `JwtHandlerAdapter` and implement only the methods you need. For example: + +```java +T returnVal = Jwts.parser().setSigningKey(key).parse(plaintextJwt, new JwtHandlerAdapter>() { + @Override + public T onPlaintextJws(Jws jws) { + //the JWT parsed was a signed plaintext JWS + //inspect it, then return an instance of T (see returnVal above) + } + + @Override + public T onClaimsJws(Jws jws) { + //the JWT parsed was a signed Claims JWS + //inspect it, then return an instance of T (see returnVal above) + } +}); +``` + +##### Known Type convenience parse methods + +If, unlike above, you are confident of the compact string format and know which type of JWT or JWS it will produce, you can just use one of the 4 new convenience parsing methods to get exactly the type of JWT or JWS you know exists. For example: + +```java + +//for a known plaintext jwt string: +Jwt jwt = Jwts.parser().parsePlaintextJwt(compact); + +//for a known Claims JWT string: +Jwt jwt = Jwts.parser().parseClaimsJwt(compact); + +//for a known signed plaintext JWT (aka a plaintext JWS): +Jws jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); + +//for a known signed Claims JWT (aka a Claims JWS): +Jws jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); + +``` + + +#### Already using an older Jackson dependency? + +JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: + +```xml + + com.fasterxml.jackson.core + jackson-databind + 2.4.2 + +``` diff --git a/README.md b/README.md index 943ebfc9..32d00485 100644 --- a/README.md +++ b/README.md @@ -105,264 +105,8 @@ These feature sets will be implemented in a future release when possible. Commu - [Where to Store Your JWTs - Cookies vs HTML5 Web Storage](https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/) - [Use JWT the Right Way!](https://stormpath.com/blog/jwt-the-right-way/) - [Token Authentication for Java Applications](https://stormpath.com/blog/token-auth-for-java/) +- [JJWT Changelog](CHANGELOG.md) -## Release Notes - -### 0.6.0 - -#### Enforce JWT Claims when Parsing - -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: - -```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(fieldName, requiredFieldValue)` 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) - -#### 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", "someReallyLongDataString...") - .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", "someReallyLongDataString...") - .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 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 - -- 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. - -### 0.5 - -- Android support! Android's built-in Base64 codec will be used if JJWT detects it is running in an Android environment. Other than Base64, all other parts of JJWT were already Android-compliant. Now it is fully compliant. - -- Elliptic Curve signature algorithms! `SignatureAlgorithm.ES256`, `ES384` and `ES512` are now supported. - -- Super convenient key generation methods, so you don't have to worry how to do this safely: - - `MacProvider.generateKey(); //or generateKey(SignatureAlgorithm)` - - `RsaProvider.generateKeyPair(); //or generateKeyPair(sizeInBits)` - - `EllipticCurveProvider.generateKeyPair(); //or generateKeyPair(SignatureAlgorithm)` - - The `generate`* methods that accept an `SignatureAlgorithm` argument know to generate a key of sufficient strength that reflects the specified algorithm strength. - -Please see the full [0.5 closed issues list](https://github.com/jwtk/jjwt/issues?q=milestone%3A0.5+is%3Aclosed) for more information. - -### 0.4 - -- [Issue 8](https://github.com/jwtk/jjwt/issues/8): Add ability to find signing key by inspecting the JWS values before verifying the signature. - -This is a handy little feature. If you need to parse a signed JWT (a JWS) and you don't know which signing key was used to sign it, you can now use the new `SigningKeyResolver` concept. - -A `SigningKeyresolver` can inspect the JWS header and body (Claims or String) _before_ the JWS signature is verified. By inspecting the data, you can find the key and return it, and the parser will use the returned key to validate the signature. For example: - -```java -SigningKeyResolver resolver = new MySigningKeyResolver(); - -Jws jws = Jwts.parser().setSigningKeyResolver(resolver).parseClaimsJws(compact); -``` - -The signature is still validated, and the JWT instance will still not be returned if the jwt string is invalid, as expected. You just get to 'see' the JWT data for key discovery before the parser validates. Nice. - -This of course requires that you put some sort of information in the JWS when you create it so that your `SigningKeyResolver` implementation can look at it later and look up the key. The *standard* way to do this is to use the JWS `kid` ('key id') field, for example: - -```java -Jwts.builder().setHeaderParam("kid", your_signing_key_id_NOT_THE_SECRET).build(); -``` - -You could of course set any other header parameter or claims parameter instead of setting `kid` if you want - that's just the default field reserved for signing key identification. If you can locate the signing key based on other information in the header or claims, you don't need to set the `kid` field - just make sure your resolver implementation knows how to look up the key. - -Finally, a nice `SigningKeyResolverAdapter` is provided to allow you to write quick and simple subclasses or anonymous classes instead of having to implement the `SigningKeyResolver` interface directly. For example: - -```java -Jws jws = Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() { - @Override - public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) { - //inspect the header or claims, lookup and return the signing key - String keyId = header.getKeyId(); //or any other field that you need to inspect - return getSigningKey(keyId); //implement me - }}) - .parseClaimsJws(compact); -``` - -### 0.3 - -- [Issue 6](https://github.com/jwtk/jjwt/issues/6): Parsing an expired Claims JWT or JWS (as determined by the `exp` claims field) will now throw an `ExpiredJwtException`. -- [Issue 7](https://github.com/jwtk/jjwt/issues/7): Parsing a premature Claims JWT or JWS (as determined by the `nbf` claims field) will now throw a `PrematureJwtException`. - -### 0.2 - -#### More convenient Claims building - -This release adds convenience methods to the `JwtBuilder` interface so you can set claims directly on the builder without having to create a separate Claims instance/builder, reducing the amount of code you have to write. For example, this: - -```java -Claims claims = Jwts.claims().setSubject("Joe"); - -String compactJwt = Jwts.builder().setClaims(claims).signWith(HS256, key).compact(); -``` - -can now be written as: - -```java -String compactJwt = Jwts.builder().setSubject("Joe").signWith(HS256, key).compact(); -``` - -A Claims instance based on the specified claims will be created and set as the JWT's payload automatically. - -#### Type-safe handling for JWT and JWS with generics - -The following < 0.2 code produced a JWT as expected: - -```java -Jwt jwt = Jwts.parser().setSigningKey(key).parse(compact); -``` - -But you couldn't easily determine if the `jwt` was a `JWT` or `JWS` instance or if the body was a `Claims` instance or a plaintext `String` without resorting to a bunch of yucky `instanceof` checks. In 0.2, we introduce the `JwtHandler` when you don't know the exact format of the compact JWT string ahead of time, and parsing convenience methods when you do. - -##### JwtHandler - -If you do not know the format of the compact JWT string at the time you try to parse it, you can determine what type it is after parsing by providing a `JwtHandler` instance to the `JwtParser` with the new `parse(String compactJwt, JwtHandler handler)` method. For example: - -```java -T returnVal = Jwts.parser().setSigningKey(key).parse(compact, new JwtHandler() { - @Override - public T onPlaintextJwt(Jwt jwt) { - //the JWT parsed was an unsigned plaintext JWT - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJwt(Jwt jwt) { - //the JWT parsed was an unsigned Claims JWT - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onPlaintextJws(Jws jws) { - //the JWT parsed was a signed plaintext JWS - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJws(Jws jws) { - //the JWT parsed was a signed Claims JWS - //inspect it, then return an instance of T (see returnVal above) - } -}); -``` - -Of course, if you know you'll only have to parse a subset of the above, you can use the `JwtHandlerAdapter` and implement only the methods you need. For example: - -```java -T returnVal = Jwts.parser().setSigningKey(key).parse(plaintextJwt, new JwtHandlerAdapter>() { - @Override - public T onPlaintextJws(Jws jws) { - //the JWT parsed was a signed plaintext JWS - //inspect it, then return an instance of T (see returnVal above) - } - - @Override - public T onClaimsJws(Jws jws) { - //the JWT parsed was a signed Claims JWS - //inspect it, then return an instance of T (see returnVal above) - } -}); -``` - -##### Known Type convenience parse methods - -If, unlike above, you are confident of the compact string format and know which type of JWT or JWS it will produce, you can just use one of the 4 new convenience parsing methods to get exactly the type of JWT or JWS you know exists. For example: - -```java - -//for a known plaintext jwt string: -Jwt jwt = Jwts.parser().parsePlaintextJwt(compact); - -//for a known Claims JWT string: -Jwt jwt = Jwts.parser().parseClaimsJwt(compact); - -//for a known signed plaintext JWT (aka a plaintext JWS): -Jws jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); - -//for a known signed Claims JWT (aka a Claims JWS): -Jws jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); - -``` - - -#### Already using an older Jackson dependency? - -JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: - -```xml - - com.fasterxml.jackson.core - jackson-databind - 2.4.2 - -``` ## Author From 78cb1707d7fe137bf27911444c3aa686c75ab205 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Fri, 17 Jun 2016 14:55:57 -0700 Subject: [PATCH 13/36] moved older jackson section back into readme --- CHANGELOG.md | 13 ------------- README.md | 12 ++++++++++++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a33cc1..188b2c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -242,16 +242,3 @@ Jws jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact); Jws jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact); ``` - - -#### Already using an older Jackson dependency? - -JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: - -```xml - - com.fasterxml.jackson.core - jackson-databind - 2.4.2 - -``` diff --git a/README.md b/README.md index 32d00485..7fa465b8 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,18 @@ These feature sets will be implemented in a future release when possible. Commu - [Token Authentication for Java Applications](https://stormpath.com/blog/token-auth-for-java/) - [JJWT Changelog](CHANGELOG.md) + +#### Already using an older Jackson dependency? + +JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: + +```xml + + com.fasterxml.jackson.core + jackson-databind + 2.4.2 + +``` ## Author From b053834dae7fbb06d0493c1cb32418cedebe34a5 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Fri, 17 Jun 2016 15:11:08 -0700 Subject: [PATCH 14/36] Updated README with more examples --- README.md | 81 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7fa465b8..0d349112 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ [![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 +## Java JWT: JSON Web Token for Java and Android 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/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. +JJWT is an implementation based 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. + +The library was created by [Stormpath's](http://www.stormpath.com) CTO, [Les Hazlewood](https://github.com/lhazlewood) +and is now maintained by a [community](https://github.com/jwtk/jjwt/graphs/contributors) of contributors. + +We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement. ## Installation @@ -31,7 +36,7 @@ dependencies { Note: JJWT depends on Jackson 2.x. If you're already using an older version of Jackson in your app, [read this](#olderJackson) -## Usage +## Quickstart Most complexity is hidden behind a convenient and readable builder-based [fluent interface](http://en.wikipedia.org/wiki/Fluent_interface), great for relying on IDE auto-completion to write code quickly. Here's an example: @@ -45,25 +50,38 @@ import java.security.Key; // the key would be read from your application configuration instead. Key key = MacProvider.generateKey(); -String s = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS512, key).compact(); +String compactJws = Jwts.builder() + .setSubject("Joe") + .signWith(SignatureAlgorithm.HS512, key) + .compact(); ``` How easy was that!? +In this case, we are *building* a JWT that will have the [registered claim](https://tools.ietf.org/html/rfc7519#section-4.1) `sub` (subject) set to `Joe`. We are signing the JWT using the HMAC using SHA-512 algorithm. finally, we are compacting it into its `String` form. + +The resultant `String` looks like this: + +``` +eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJKb2UifQ.yiV1GWDrQyCeoOswYTf_xvlgsnaVVYJM0mU6rkmRBf2T1MBl3Xh2kZii0Q9BdX5-G0j25Qv2WF4lA6jPl5GKuA +``` + Now let's verify the JWT (you should always discard JWTs that don't match an expected signature): ```java -assert Jwts.parser().setSigningKey(key).parseClaimsJws(s).getBody().getSubject().equals("Joe"); +assert Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject().equals("Joe"); ``` -You have to love one-line code snippets! +There are two things going on here. The `key` from before is being used to validate the signature of the JWT. If it fails to verify the JWT, a `SignatureException` is thrown. Assuming the JWT is validated, we parse out the claims and assert that that subject is set to `Joe`. + +You have to love code one-liners that pack a punch! But what if signature validation failed? You can catch `SignatureException` and react accordingly: ```java try { - Jwts.parser().setSigningKey(key).parseClaimsJws(compactJwt); + Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws); //OK, we can trust this JWT @@ -75,6 +93,8 @@ try { ## Supported Features +### Specification Compliant: + * Creating and parsing plaintext compact JWTs * Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms: @@ -91,12 +111,55 @@ try { * ES384: ECDSA using P-384 and SHA-384 * ES512: ECDSA using P-512 and SHA-512 +### Enhancements Beyond the Specification: + +* Body compression. If the JWT body is large, you can use a `CompressionCodec` to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding. + +```java +String compactJws = Jwts.builder() + .setSubject("Joe") + .compressWith(CompressionCodecs.DEFLATE) + .signWith(SignatureAlgorithm.HS512, key) + .compact(); +``` + +If you examine the header section of the `compactJws`, it decodes to this: + +``` +{ + "alg": "HS512", + "zip": "DEF" +} +``` + +JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression. + +* Require Claims. When parsing, you can specify that certain calims *must* be present and set to a certain value. + +```java +try { + Jws claims = Jwts.parser() + .requireSubject("Joe") + .require("hasMotorcycle", true) + .setSigningKey(key) + .parseClaimsJws(compactJws); +} catch (MissingClaimException e) { + + // we get here if the required claim is not present + +} catch (IncorrectClaimException) { + + // we get here if ther required claim has the wrong value + +} +``` + ## Currently Unsupported Features * [Non-compact](https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-7.2) serialization and parsing. * JWE (Encryption for JWT) -These feature sets will be implemented in a future release when possible. Community contributions are welcome! +These feature sets will be implemented in a future release. Community contributions are welcome! ## Learn More @@ -126,4 +189,4 @@ Maintained by [Stormpath](https://stormpath.com/) ## Licensing -This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file +This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). From 7a2808af129d1ddbf2f507fb5a4fc6d4006879a1 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sat, 2 Jul 2016 17:09:22 -0400 Subject: [PATCH 15/36] Expanded on intro section. --- README.md | 118 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 0d349112..7fa399e0 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,57 @@ JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM. -JJWT is an implementation based 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. +JJWT is a Java implementation based 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. The library was created by [Stormpath's](http://www.stormpath.com) CTO, [Les Hazlewood](https://github.com/lhazlewood) and is now maintained by a [community](https://github.com/jwtk/jjwt/graphs/contributors) of contributors. We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement. +## What's a JSON Web Token? + +Don't know what a JSON Web Token is? Read on. Otherwise, jump on down to the [Installation](#installation) section. + +JWT is a means of transmitting information between two parties in a compact, verifiable form. + +The bits of information encoded in the body of a JWT are called `claims`. The expanded form of the JWT is in a JSON format, so each `claim` is a key in the JSON object. + +JWTs can be cryptographically signed (making it a [JWS](https://tools.ietf.org/html/rfc7515)) or encrypted (making it a [JWE](https://tools.ietf.org/html/rfc7516)). + +This adds a powerful layer of verifiability to the user of JWTs. The receiver has a high degree of confidence that the JWT has not been tampered with by verifying the signature, for instance. + +The compacted representation of a signed JWT is a string that has three parts, each separated by a `.`: + +``` +eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY +``` + +Each section is [base 64](https://en.wikipedia.org/wiki/Base64) encoded. The first section is the header, which at a minimum needs to specify the algorithm used to sign the JWT. The second section is the body. This section has all the claims of this JWT encoded in it. The final section is the signature. It's computed by passing a combination of the header and body through the algorithm specified in the header. + +If you pass the first two sections through a base 64 decoder, you'll get the following (formatting added for clarity): + +`header` +``` +{ + "alg": "HS256" +} +``` + +`body` +``` +{ + "sub": "Joe" +} +``` + +In this case, the information we have is that the HMAC using SHA-256 algorithm was used to sign the JWT. And, the body has a single claim, `sub` with value `Joe`. + +There are a number of standard claims, called [Registered Claims](https://tools.ietf.org/html/rfc7519#section-4.1), in the specification and `sub` (for subject) is one of them. + +To compute the signature, you must know the secret that was used to sign it. In this case, it was the word `secret`. You can see the signature creation is action [here](https://jsfiddle.net/dogeared/2fy2y0yd/11/) (Note: Trailing `=` are lopped off the signature for the JWT). + +Now you know (just about) all you need to know about JWTs. + ## Installation Use your favorite Maven-compatible build tool to pull the dependency (and its transitive dependencies) from Maven Central: @@ -113,46 +157,46 @@ try { ### Enhancements Beyond the Specification: -* Body compression. If the JWT body is large, you can use a `CompressionCodec` to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding. +* **Body compression.** If the JWT body is large, you can use a `CompressionCodec` to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding. -```java -String compactJws = Jwts.builder() - .setSubject("Joe") - .compressWith(CompressionCodecs.DEFLATE) - .signWith(SignatureAlgorithm.HS512, key) - .compact(); -``` + ```java + String compactJws = Jwts.builder() + .setSubject("Joe") + .compressWith(CompressionCodecs.DEFLATE) + .signWith(SignatureAlgorithm.HS512, key) + .compact(); + ``` -If you examine the header section of the `compactJws`, it decodes to this: + If you examine the header section of the `compactJws`, it decodes to this: + + ``` + { + "alg": "HS512", + "zip": "DEF" + } + ``` + + JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression. -``` -{ - "alg": "HS512", - "zip": "DEF" -} -``` +* **Require Claims.** When parsing, you can specify that certain calims *must* be present and set to a certain value. -JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression. - -* Require Claims. When parsing, you can specify that certain calims *must* be present and set to a certain value. - -```java -try { - Jws claims = Jwts.parser() - .requireSubject("Joe") - .require("hasMotorcycle", true) - .setSigningKey(key) - .parseClaimsJws(compactJws); -} catch (MissingClaimException e) { - - // we get here if the required claim is not present - -} catch (IncorrectClaimException) { - - // we get here if ther required claim has the wrong value - -} -``` + ```java + try { + Jws claims = Jwts.parser() + .requireSubject("Joe") + .require("hasMotorcycle", true) + .setSigningKey(key) + .parseClaimsJws(compactJws); + } catch (MissingClaimException e) { + + // we get here if the required claim is not present + + } catch (IncorrectClaimException) { + + // we get here if ther required claim has the wrong value + + } + ``` ## Currently Unsupported Features From 82f4b0a696479cce5d595bf5b1e94c9e8193a930 Mon Sep 17 00:00:00 2001 From: Micah Silverman Date: Sun, 3 Jul 2016 23:38:25 -0400 Subject: [PATCH 16/36] updated to jacoco as only jacoco supports java 8 per: https://github.com/trautonen/coveralls-maven-plugin#faq --- .travis.yml | 4 ++-- pom.xml | 65 +++++++---------------------------------------------- 2 files changed, 10 insertions(+), 59 deletions(-) diff --git a/.travis.yml b/.travis.yml index af0c7afa..197d2a00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ jdk: - oraclejdk8 before_install: - - export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'openjdk7' ] && echo 'true')" + - export BUILD_COVERAGE="$([ $TRAVIS_JDK_VERSION == 'oraclejdk8' ] && 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 + - test -z "$BUILD_COVERAGE" || mvn clean test jacoco:report coveralls:report diff --git a/pom.xml b/pom.xml index 0d11dbac..d44630fe 100644 --- a/pom.xml +++ b/pom.xml @@ -270,51 +270,19 @@ - org.codehaus.mojo - cobertura-maven-plugin - 2.7 + org.jacoco + jacoco-maven-plugin + 0.7.6.201602180812 - 256m - true - - - io.jsonwebtoken.lang.* - - - - 100 - 100 - - true - - - - io.jsonwebtoken.lang.* - 0 - 0 - - - - io.jsonwebtoken.impl.DefaultClaims - 96 - 100 - - - io.jsonwebtoken.impl.DefaultJwtParser - 100 - 90 - - - + + **/io/jsonwebtoken/lang/* + + prepare-agent - clean - check + prepare-agent @@ -436,21 +404,4 @@ - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.7 - - - xml - html - - - - - - From 3bd425a63dde3d52309cac45cf09822dd5f516d6 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 4 Jul 2016 12:16:16 -0700 Subject: [PATCH 17/36] updated coveralls logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fa399e0..d0595c17 100644 --- a/README.md +++ b/README.md @@ -1,5 +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) +[![Coverage Status](https://coveralls.io/repos/github/jwtk/jjwt/badge.svg?branch=master)](https://coveralls.io/github/jwtk/jjwt?branch=master) ## Java JWT: JSON Web Token for Java and Android From ab76c850dbd1559568410cf8eabb4876512629e9 Mon Sep 17 00:00:00 2001 From: brentstormpath Date: Tue, 12 Jul 2016 17:24:26 -0700 Subject: [PATCH 18/36] Readme Update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d0595c17..8033d355 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ JJWT is a Java implementation based on the [JWT](https://tools.ietf.org/html/rfc The library was created by [Stormpath's](http://www.stormpath.com) CTO, [Les Hazlewood](https://github.com/lhazlewood) and is now maintained by a [community](https://github.com/jwtk/jjwt/graphs/contributors) of contributors. +[Stormpath](https://stormpath.com/) is a complete authentication and user management API for developers. + We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement. ## What's a JSON Web Token? From c5ae6f53f1ac638dfdf72315d552d00b034d3a31 Mon Sep 17 00:00:00 2001 From: Michael Collis Date: Thu, 21 Jul 2016 15:30:36 -0400 Subject: [PATCH 19/36] Fix ES512 description typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0595c17..ca032fbb 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ try { * PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 * ES256: ECDSA using P-256 and SHA-256 * ES384: ECDSA using P-384 and SHA-384 - * ES512: ECDSA using P-512 and SHA-512 + * ES512: ECDSA using P-521 and SHA-512 ### Enhancements Beyond the Specification: From 3fb794ee9131b9e6d6f0c24e408b05e53025aa79 Mon Sep 17 00:00:00 2001 From: Michael Sims Date: Wed, 27 Jan 2016 13:39:34 -0600 Subject: [PATCH 20/36] #61: Add support for clock skew to JwtParser for exp and nbf claims --- src/main/java/io/jsonwebtoken/JwtParser.java | 10 ++++ .../jsonwebtoken/impl/DefaultJwtParser.java | 22 ++++++-- .../io/jsonwebtoken/JwtParserTest.groovy | 52 +++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index e0373093..68889180 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -136,6 +136,16 @@ public interface JwtParser { */ JwtParser setClock(Clock clock); + /** + * Sets the amount of clock skew in seconds to tolerate when verifying the local time against the {@code exp} + * and {@code nbf} claims. + * + * @param seconds + * @return the parser for method chaining. + * @since 0.7.0 + */ + JwtParser setAllowedClockSkewInSeconds(int seconds); + /** * 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/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index da2c4582..1db5074f 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -57,6 +57,7 @@ public class DefaultJwtParser implements JwtParser { //don't need millis since JWT date fields are only second granularity: private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final int MILLISECONDS_PER_SECOND = 1000; private ObjectMapper objectMapper = new ObjectMapper(); @@ -72,6 +73,8 @@ public class DefaultJwtParser implements JwtParser { private Clock clock = DefaultClock.INSTANCE; + private int allowedClockSkewInSeconds = 0; + @Override public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); @@ -137,6 +140,12 @@ public class DefaultJwtParser implements JwtParser { return this; } + @Override + public JwtParser setAllowedClockSkewInSeconds(int seconds) { + allowedClockSkewInSeconds = seconds; + return this; + } + @Override public JwtParser setSigningKey(byte[] key) { Assert.notEmpty(key, "signing key cannot be null or empty."); @@ -356,6 +365,7 @@ public class DefaultJwtParser implements JwtParser { //since 0.3: if (claims != null) { + long allowedClockSkewInMilliseconds = (long) allowedClockSkewInSeconds * MILLISECONDS_PER_SECOND; SimpleDateFormat sdf; final Date now = this.clock.now(); @@ -365,12 +375,14 @@ public class DefaultJwtParser implements JwtParser { Date exp = claims.getExpiration(); if (exp != null) { - if (now.equals(exp) || now.after(exp)) { + Date nowWithAllowedClockSkew = new Date(now.getTime() - allowedClockSkewInMilliseconds); + if (nowWithAllowedClockSkew.equals(exp) || nowWithAllowedClockSkew.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = sdf.format(exp); String nowVal = sdf.format(now); - String msg = "JWT expired at " + expVal + ". Current time: " + nowVal; + String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + + ". Allowed clock skew: " + allowedClockSkewInSeconds + " second(s)."; throw new ExpiredJwtException(header, claims, msg); } } @@ -380,12 +392,14 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - if (now.before(nbf)) { + Date nowWithAllowedClockSkew = new Date(now.getTime() + allowedClockSkewInMilliseconds); + if (nowWithAllowedClockSkew.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = sdf.format(nbf); String nowVal = sdf.format(now); - String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal; + String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + + ". Allowed clock skew: " + allowedClockSkewInSeconds + " second(s)."; throw new PrematureJwtException(header, claims, msg); } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 40dba0c7..e0751998 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -193,6 +193,58 @@ class JwtParserTest { } } + @Test + void testParseWithExpiredJwtWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() - 3000) + + String subject = 'Joe' + String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() + + Jwt jwt = Jwts.parser().setAllowedClockSkewInSeconds(10).parse(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseWithExpiredJwtNotWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() - 3000) + + String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() + + try { + Jwts.parser().setAllowedClockSkewInSeconds(1).parse(compact) + fail() + } catch (ExpiredJwtException e) { + assertTrue e.getMessage().startsWith('JWT expired at ') + } + } + + @Test + void testParseWithPrematureJwtWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() + 3000) + + String subject = 'Joe' + String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() + + Jwt jwt = Jwts.parser().setAllowedClockSkewInSeconds(10).parse(compact) + + assertEquals jwt.getBody().getSubject(), subject + } + + @Test + void testParseWithPrematureJwtNotWithinAllowedClockSkew() { + Date exp = new Date(System.currentTimeMillis() + 3000) + + String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() + + try { + Jwts.parser().setAllowedClockSkewInSeconds(1).parse(compact) + fail() + } catch (PrematureJwtException e) { + assertTrue e.getMessage().startsWith('JWT must not be accepted before ') + } + } + // ======================================================================== // parsePlaintextJwt tests // ======================================================================== From 9735d1ad98678b15b04f65bd8f88daa32f6f5618 Mon Sep 17 00:00:00 2001 From: benoit Date: Wed, 31 Aug 2016 16:39:42 +0200 Subject: [PATCH 21/36] improve jwt parser memory allocation re-use buffer instead of creating new ones avoid creating unneeded buffers in the Strings util methods Stop continuously copying array with StringBuilder#deleteCharAt work directly on StringBuilder instead of creating a temporary String test added to cover the modified methods --- .../jsonwebtoken/impl/DefaultJwtParser.java | 5 ++- .../java/io/jsonwebtoken/lang/Strings.java | 30 +++++++++---- .../io/jsonwebtoken/lang/StringsTest.groovy | 42 +++++++++++++++++++ 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index da2c4582..50df0c7e 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -213,7 +213,8 @@ public class DefaultJwtParser implements JwtParser { if (c == SEPARATOR_CHAR) { - String token = Strings.clean(sb.toString()); + CharSequence tokenSeq = Strings.clean(sb); + String token = tokenSeq!=null?tokenSeq.toString():null; if (delimiterCount == 0) { base64UrlEncodedHeader = token; @@ -222,7 +223,7 @@ public class DefaultJwtParser implements JwtParser { } delimiterCount++; - sb = new StringBuilder(128); + sb.setLength(0); } else { sb.append(c); } diff --git a/src/main/java/io/jsonwebtoken/lang/Strings.java b/src/main/java/io/jsonwebtoken/lang/Strings.java index d1974260..a93cf340 100644 --- a/src/main/java/io/jsonwebtoken/lang/Strings.java +++ b/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -159,22 +159,38 @@ public final class Strings { * @see java.lang.Character#isWhitespace */ public static String trimWhitespace(String str) { + return (String) trimWhitespace((CharSequence)str); + } + + + private static CharSequence trimWhitespace(CharSequence str) { if (!hasLength(str)) { return str; } - StringBuilder sb = new StringBuilder(str); - while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { - sb.deleteCharAt(0); + final int length = str.length(); + + int start = 0; + while (start < length && Character.isWhitespace(str.charAt(start))) { + start++; } - while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { - sb.deleteCharAt(sb.length() - 1); + + int end = length; + while (start < length && Character.isWhitespace(str.charAt(end - 1))) { + end--; } - return sb.toString(); + + return ((start > 0) || (end < length)) ? str.subSequence(start, end) : str; } public static String clean(String str) { + CharSequence result = clean((CharSequence) str); + + return result!=null?result.toString():null; + } + + public static CharSequence clean(CharSequence str) { str = trimWhitespace(str); - if ("".equals(str)) { + if (!hasLength(str)) { return null; } return str; diff --git a/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy b/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy index a06f5d57..def31169 100644 --- a/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/lang/StringsTest.groovy @@ -14,4 +14,46 @@ class StringsTest { assertTrue Strings.hasText(" foo "); assertTrue Strings.hasText("foo") } + + @Test + void testClean() { + assertEquals "this is a test", Strings.clean("this is a test") + assertEquals "this is a test", Strings.clean(" this is a test") + assertEquals "this is a test", Strings.clean(" this is a test ") + assertEquals "this is a test", Strings.clean("\nthis is a test \t ") + assertNull Strings.clean(null) + assertNull Strings.clean("") + assertNull Strings.clean("\t") + assertNull Strings.clean(" ") + } + + @Test + void testCleanCharSequence() { + def result = Strings.clean(new StringBuilder("this is a test")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder(" this is a test")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder(" this is a test ")) + assertNotNull result + assertEquals "this is a test", result.toString() + + result = Strings.clean(new StringBuilder("\nthis is a test \t ")) + assertNotNull result + assertEquals "this is a test", result.toString() + + assertNull Strings.clean((StringBuilder) null) + assertNull Strings.clean(new StringBuilder("")) + assertNull Strings.clean(new StringBuilder("\t")) + assertNull Strings.clean(new StringBuilder(" ")) + } + + + @Test + void testTrimWhitespace() { + assertEquals "", Strings.trimWhitespace(" ") + } } From d13d2eeffe3a2d3cb46adb6443ab25090cb04d6c Mon Sep 17 00:00:00 2001 From: benoit Date: Wed, 31 Aug 2016 16:54:10 +0200 Subject: [PATCH 22/36] add eclipse files to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 611af4c7..bc177c84 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ target/ *.iml *.iws +.classpath +.project +.settings From 77dcd9a9b388d37e3a642c3704e20e880d12ffa9 Mon Sep 17 00:00:00 2001 From: Mauro Ciancio Date: Thu, 8 Sep 2016 11:56:17 -0300 Subject: [PATCH 23/36] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca032fbb..aaa50c54 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ try { JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression. -* **Require Claims.** When parsing, you can specify that certain calims *must* be present and set to a certain value. +* **Require Claims.** When parsing, you can specify that certain claims *must* be present and set to a certain value. ```java try { From 79e95856a4f9fd830d1809d931e027023c1e549c Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Sun, 11 Sep 2016 12:48:48 -0700 Subject: [PATCH 24/36] 161: upgraded library versions to latest stable --- pom.xml | 18 +++++++++--------- .../io/jsonwebtoken/JwtParserTest.groovy | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index d44630fe..fc186277 100644 --- a/pom.xml +++ b/pom.xml @@ -54,25 +54,25 @@ - 2.4 - 3.1 + 3.0.2 + 3.5.1 1.6 UTF-8 ${user.name}-${maven.build.timestamp} - 2.7.0 + 2.8.2 - 1.51 + 1.55 - 2.3.0-beta-2 - 1.0.7 - 3.3.1 + 2.4.7 + 1.1.7 + 3.4 4.12 - 1.6.2 - 2.12.4 + 1.6.5 + 2.19.1 diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 40dba0c7..528656dc 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -888,7 +888,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def issuedAtMillis = ((long)issuedAt.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis + assertEquals jwt.getBody().getIssuedAt().getTime(), issuedAtMillis, 0 } @Test @@ -1212,7 +1212,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def expirationMillis = ((long)expiration.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis + assertEquals jwt.getBody().getExpiration().getTime(), expirationMillis, 0 } @Test @@ -1280,7 +1280,7 @@ class JwtParserTest { // system converts to seconds (lopping off millis precision), then returns millis def notBeforeMillis = ((long)notBefore.getTime() / 1000) * 1000 - assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis + assertEquals jwt.getBody().getNotBefore().getTime(), notBeforeMillis, 0 } @Test From 19740695616b827f484a96344757fb07eee0db5c Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Sun, 11 Sep 2016 14:04:20 -0700 Subject: [PATCH 25/36] 107: ensured exception message printed UTC times correctly --- src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java | 2 +- src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 50df0c7e..90e9923a 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -56,7 +56,7 @@ import java.util.Map; public class DefaultJwtParser implements JwtParser { //don't need millis since JWT date fields are only second granularity: - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private ObjectMapper objectMapper = new ObjectMapper(); diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 528656dc..5884e9a9 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -175,6 +175,9 @@ class JwtParserTest { fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') + + //https://github.com/jwtk/jjwt/issues/107 : + assertTrue e.getMessage().endsWith('Z') } } @@ -190,6 +193,9 @@ class JwtParserTest { fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') + + //https://github.com/jwtk/jjwt/issues/107 : + assertTrue e.getMessage().endsWith('Z') } } From af01cca9224371012f9240c756a6a8ecbf606074 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 10:37:34 -0700 Subject: [PATCH 26/36] 122: added code comments so readers understand that JWT mandates seconds, not milliseconds --- src/main/java/io/jsonwebtoken/impl/JwtMap.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java index ba702d7c..2c3329cb 100644 --- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -47,10 +47,16 @@ public class JwtMap implements Map { } else if (v instanceof Date) { return (Date) v; } else if (v instanceof Number) { + // https://github.com/jwtk/jjwt/issues/122: + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = ((Number) v).longValue(); long millis = seconds * 1000; return new Date(millis); } else if (v instanceof String) { + // https://github.com/jwtk/jjwt/issues/122 + // The JWT RFC *mandates* NumericDate values are represented as seconds. + // Because Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = Long.parseLong((String) v); long millis = seconds * 1000; return new Date(millis); From 8f1b528d8c6593b4a39c0b5e7cc686c8a7ac92e8 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 16:12:30 -0700 Subject: [PATCH 27/36] Minor edits to @MichaelSims pull request - prepping for release --- src/main/java/io/jsonwebtoken/JwtParser.java | 6 +++--- .../io/jsonwebtoken/impl/DefaultJwtParser.java | 16 ++++++++-------- .../groovy/io/jsonwebtoken/JwtParserTest.groovy | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java index 68889180..fd1f6c7e 100644 --- a/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -131,7 +131,7 @@ public interface JwtParser { * The parser uses a {@link DefaultClock DefaultClock} instance by default. * * @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT. - * @return the builder instance for method chaining. + * @return the parser for method chaining. * @since 0.7.0 */ JwtParser setClock(Clock clock); @@ -140,11 +140,11 @@ public interface JwtParser { * Sets the amount of clock skew in seconds to tolerate when verifying the local time against the {@code exp} * and {@code nbf} claims. * - * @param seconds + * @param seconds the number of seconds to tolerate for clock skew when verifying {@code exp} or {@code nbf} claims. * @return the parser for method chaining. * @since 0.7.0 */ - JwtParser setAllowedClockSkewInSeconds(int seconds); + JwtParser setAllowedClockSkewSeconds(long seconds); /** * 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 624333d8..ef10f962 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -73,7 +73,7 @@ public class DefaultJwtParser implements JwtParser { private Clock clock = DefaultClock.INSTANCE; - private int allowedClockSkewInSeconds = 0; + private long allowedClockSkewSeconds = 0; @Override public JwtParser requireIssuedAt(Date issuedAt) { @@ -141,8 +141,8 @@ public class DefaultJwtParser implements JwtParser { } @Override - public JwtParser setAllowedClockSkewInSeconds(int seconds) { - allowedClockSkewInSeconds = seconds; + public JwtParser setAllowedClockSkewSeconds(long seconds) { + allowedClockSkewSeconds = seconds; return this; } @@ -366,7 +366,7 @@ public class DefaultJwtParser implements JwtParser { //since 0.3: if (claims != null) { - long allowedClockSkewInMilliseconds = (long) allowedClockSkewInSeconds * MILLISECONDS_PER_SECOND; + long allowedClockSkewMillis = allowedClockSkewSeconds * MILLISECONDS_PER_SECOND; SimpleDateFormat sdf; final Date now = this.clock.now(); @@ -376,14 +376,14 @@ public class DefaultJwtParser implements JwtParser { Date exp = claims.getExpiration(); if (exp != null) { - Date nowWithAllowedClockSkew = new Date(now.getTime() - allowedClockSkewInMilliseconds); + Date nowWithAllowedClockSkew = new Date(now.getTime() - allowedClockSkewMillis); if (nowWithAllowedClockSkew.equals(exp) || nowWithAllowedClockSkew.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = sdf.format(exp); String nowVal = sdf.format(now); String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + - ". Allowed clock skew: " + allowedClockSkewInSeconds + " second(s)."; + ". Allowed clock skew: " + allowedClockSkewSeconds + " second(s)."; throw new ExpiredJwtException(header, claims, msg); } } @@ -393,14 +393,14 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - Date nowWithAllowedClockSkew = new Date(now.getTime() + allowedClockSkewInMilliseconds); + Date nowWithAllowedClockSkew = new Date(now.getTime() + allowedClockSkewMillis); if (nowWithAllowedClockSkew.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = sdf.format(nbf); String nowVal = sdf.format(now); String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + - ". Allowed clock skew: " + allowedClockSkewInSeconds + " second(s)."; + ". Allowed clock skew: " + allowedClockSkewSeconds + " second(s)."; throw new PrematureJwtException(header, claims, msg); } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 2b2fcf0a..d7649487 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -206,7 +206,7 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setExpiration(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewInSeconds(10).parse(compact) + Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -218,7 +218,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() try { - Jwts.parser().setAllowedClockSkewInSeconds(1).parse(compact) + Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) fail() } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') @@ -232,7 +232,7 @@ class JwtParserTest { String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() - Jwt jwt = Jwts.parser().setAllowedClockSkewInSeconds(10).parse(compact) + Jwt jwt = Jwts.parser().setAllowedClockSkewSeconds(10).parse(compact) assertEquals jwt.getBody().getSubject(), subject } @@ -244,7 +244,7 @@ class JwtParserTest { String compact = Jwts.builder().setSubject('Joe').setNotBefore(exp).compact() try { - Jwts.parser().setAllowedClockSkewInSeconds(1).parse(compact) + Jwts.parser().setAllowedClockSkewSeconds(1).parse(compact) fail() } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') From ab4f9ff9e85c0bda785026e1ad04fc448a710e65 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 16:39:17 -0700 Subject: [PATCH 28/36] edits to exception message to be a little more helpful and to ensure previous GH issue tests passed --- .../jsonwebtoken/impl/DefaultJwtParser.java | 39 ++++++++++--------- .../io/jsonwebtoken/JwtParserTest.groovy | 8 ++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ef10f962..c5cabb43 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -73,54 +73,47 @@ public class DefaultJwtParser implements JwtParser { private Clock clock = DefaultClock.INSTANCE; - private long allowedClockSkewSeconds = 0; + private long allowedClockSkewMillis = 0; @Override public JwtParser requireIssuedAt(Date issuedAt) { expectedClaims.setIssuedAt(issuedAt); - return this; } @Override public JwtParser requireIssuer(String issuer) { expectedClaims.setIssuer(issuer); - return this; } @Override public JwtParser requireAudience(String audience) { expectedClaims.setAudience(audience); - return this; } @Override public JwtParser requireSubject(String subject) { expectedClaims.setSubject(subject); - return this; } @Override public JwtParser requireId(String id) { expectedClaims.setId(id); - return this; } @Override public JwtParser requireExpiration(Date expiration) { expectedClaims.setExpiration(expiration); - return this; } @Override public JwtParser requireNotBefore(Date notBefore) { expectedClaims.setNotBefore(notBefore); - return this; } @@ -129,7 +122,6 @@ public class DefaultJwtParser implements JwtParser { 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; } @@ -142,7 +134,7 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser setAllowedClockSkewSeconds(long seconds) { - allowedClockSkewSeconds = seconds; + allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND); return this; } @@ -363,27 +355,33 @@ public class DefaultJwtParser implements JwtParser { } } + final boolean allowSkew = allowedClockSkewMillis > 0; + //since 0.3: if (claims != null) { - long allowedClockSkewMillis = allowedClockSkewSeconds * MILLISECONDS_PER_SECOND; SimpleDateFormat sdf; final Date now = this.clock.now(); + long nowTime = now.getTime(); //https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-4.1.4 //token MUST NOT be accepted on or after any specified exp time: Date exp = claims.getExpiration(); if (exp != null) { - Date nowWithAllowedClockSkew = new Date(now.getTime() - allowedClockSkewMillis); - if (nowWithAllowedClockSkew.equals(exp) || nowWithAllowedClockSkew.after(exp)) { + long maxTime = nowTime - allowedClockSkewMillis; + Date max = allowSkew ? new Date(maxTime) : now; + if (max.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String expVal = sdf.format(exp); String nowVal = sdf.format(now); - String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + - ". Allowed clock skew: " + allowedClockSkewSeconds + " second(s)."; + long differenceMillis = maxTime - exp.getTime(); + + String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException(header, claims, msg); } } @@ -393,14 +391,19 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - Date nowWithAllowedClockSkew = new Date(now.getTime() + allowedClockSkewMillis); - if (nowWithAllowedClockSkew.before(nbf)) { + long minTime = nowTime + allowedClockSkewMillis; + Date min = allowSkew ? new Date(minTime) : now; + if (min.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); String nbfVal = sdf.format(nbf); String nowVal = sdf.format(now); + long differenceMillis = nbf.getTime() - minTime; + String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + - ". Allowed clock skew: " + allowedClockSkewSeconds + " second(s)."; + ", a difference of " + + differenceMillis + " milliseconds. Allowed clock skew: " + + allowedClockSkewMillis + " milliseconds."; throw new PrematureJwtException(header, claims, msg); } } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index d7649487..f3a0368d 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -176,8 +176,8 @@ class JwtParserTest { } catch (ExpiredJwtException e) { assertTrue e.getMessage().startsWith('JWT expired at ') - //https://github.com/jwtk/jjwt/issues/107 : - assertTrue e.getMessage().endsWith('Z') + //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): + assertTrue e.getMessage().contains('Z, a difference of ') } } @@ -194,8 +194,8 @@ class JwtParserTest { } catch (PrematureJwtException e) { assertTrue e.getMessage().startsWith('JWT must not be accepted before ') - //https://github.com/jwtk/jjwt/issues/107 : - assertTrue e.getMessage().endsWith('Z') + //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): + assertTrue e.getMessage().contains('Z, a difference of ') } } From 6c4b58e4fe1bdae811560cb6b5234d65e2c553f3 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 16:40:52 -0700 Subject: [PATCH 29/36] edits to exception message to be a little more helpful and to ensure previous GH issue tests passed --- .../java/io/jsonwebtoken/impl/DefaultJwtParser.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index c5cabb43..4e4b9c79 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -134,7 +134,7 @@ public class DefaultJwtParser implements JwtParser { @Override public JwtParser setAllowedClockSkewSeconds(long seconds) { - allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND); + this.allowedClockSkewMillis = Math.max(0, seconds * MILLISECONDS_PER_SECOND); return this; } @@ -355,7 +355,7 @@ public class DefaultJwtParser implements JwtParser { } } - final boolean allowSkew = allowedClockSkewMillis > 0; + final boolean allowSkew = this.allowedClockSkewMillis > 0; //since 0.3: if (claims != null) { @@ -370,7 +370,7 @@ public class DefaultJwtParser implements JwtParser { Date exp = claims.getExpiration(); if (exp != null) { - long maxTime = nowTime - allowedClockSkewMillis; + long maxTime = nowTime - this.allowedClockSkewMillis; Date max = allowSkew ? new Date(maxTime) : now; if (max.after(exp)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); @@ -381,7 +381,7 @@ public class DefaultJwtParser implements JwtParser { String msg = "JWT expired at " + expVal + ". Current time: " + nowVal + ", a difference of " + differenceMillis + " milliseconds. Allowed clock skew: " + - allowedClockSkewMillis + " milliseconds."; + this.allowedClockSkewMillis + " milliseconds."; throw new ExpiredJwtException(header, claims, msg); } } @@ -391,7 +391,7 @@ public class DefaultJwtParser implements JwtParser { Date nbf = claims.getNotBefore(); if (nbf != null) { - long minTime = nowTime + allowedClockSkewMillis; + long minTime = nowTime + this.allowedClockSkewMillis; Date min = allowSkew ? new Date(minTime) : now; if (min.before(nbf)) { sdf = new SimpleDateFormat(ISO_8601_FORMAT); @@ -403,7 +403,7 @@ public class DefaultJwtParser implements JwtParser { String msg = "JWT must not be accepted before " + nbfVal + ". Current time: " + nowVal + ", a difference of " + differenceMillis + " milliseconds. Allowed clock skew: " + - allowedClockSkewMillis + " milliseconds."; + this.allowedClockSkewMillis + " milliseconds."; throw new PrematureJwtException(header, claims, msg); } } From c13362dafaff6eae1bb80817d0dbe79449e3375e Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:20:47 -0700 Subject: [PATCH 30/36] Added release notes and doc update for the 0.7.0 release. --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188b2c58..21a6d1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ ## Release Notes +### 0.7.0 + +This is a minor feature enhancement and bugfix release. One of the bug fixes is particularly important if using +elliptic curve signatures. + +#### Elliptic Curve Signature Length Bug Fix + +Previous versions of JJWT safely calculated and verified Elliptic Curve signatures (no security risks), however, the + signatures were encoded using the JVM's default ASN.1/DER format. The JWS specification however +requires EC signatures to be in a R + S format. JJWT >= 0.7.0 now correctly represents newly computed EC signatures in +this spec-compliant format. + +What does this mean for you? + +Signatures created from previous JJWT versions can still be verified, so your existing tokens will still be parsed +correctly. HOWEVER, new JWTs with EC signatures created by JJWT >= 0.7.0 are now spec compliant and therefore can only +be verified by JJWT >= 0.7.0 (or any other spec compliant library). + +**This means that if you generate JWTs using Elliptic Curve Signatures after upgrading to JJWT >= 0.7.0, you _must_ +also upgrade any applications that parse these JWTs to upgrade to JJWT >= 0.7.0 as well.** + +#### Clock Skew Support + +When parsing a JWT, you might find that `exp` or `nbf` claims fail because the clock on the parsing machine is not +perfectly in sync with the clock on the machine that created the JWT. You can now account for these differences +(usually no more than a few minutes) when parsing using the new `setAllowedClockSkewSeconds` method on the parser. +For example: + +```java +long seconds = 3 * 60; //3 minutes +Jwts.parser().setAllowedClockSkewSeconds(seconds).setSigningKey(key).parseClaimsJws(jwt); +``` + +This ensures that clock differences between machines can be ignored. Two or three minutes should be more than enough; it +would be very strange if a machine's clock was more than 5 minutes difference from most atomic clocks around the world. + +#### Custom Clock Support + +Timestamps created during parsing can now be obtained via a custom time source via an implementation of + the new `io.jsonwebtoken.Clock` interface. The default implementation simply returns `new Date()` to reflect the time + when parsing occurs, as most would expect. However, supplying your own clock could be useful, especially during test + cases to guarantee deterministic behavior. + +#### Android RSA Private Key Support + +Previous versions of JJWT required RSA private keys to implement `java.security.interfaces.RSAPrivateKey`, but Android +6 RSA private keys do not implement this interface. JJWT now asserts that RSA keys are instances of both +`java.security.interfaces.RSAKey` and `java.security.PrivateKey` which should work fine on both Android and all other +'standard' JVMs as well. + +#### Library version updates + +The few dependencies JWWT has (e.g. Jackson) have been updated to their latest stable versions at the time of release. + ### 0.6.0 #### Enforce JWT Claims when Parsing diff --git a/README.md b/README.md index aaa50c54..6e16008a 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Maven: io.jsonwebtoken jjwt - 0.6.0 + 0.7.0 ``` @@ -74,7 +74,7 @@ Gradle: ```groovy dependencies { - compile 'io.jsonwebtoken:jjwt:0.6.0' + compile 'io.jsonwebtoken:jjwt:0.7.0' } ``` From 0da903f2144db02bc284ff47457b326f49d1712e Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:22:41 -0700 Subject: [PATCH 31/36] Added release notes and doc update for the 0.7.0 release. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a6d1d0..c702ad84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,10 @@ Previous versions of JJWT required RSA private keys to implement `java.security. The few dependencies JWWT has (e.g. Jackson) have been updated to their latest stable versions at the time of release. +#### Issue List + +For all completed issues, please see the [0.7.0 Milestone List](https://github.com/jwtk/jjwt/milestone/7?closed=1) + ### 0.6.0 #### Enforce JWT Claims when Parsing From cfeeb6e5cd4bf92216b2ce3ba7163d512d59a3c7 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:23:18 -0700 Subject: [PATCH 32/36] Added release notes and doc update for the 0.7.0 release. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c702ad84..68022b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ### 0.7.0 This is a minor feature enhancement and bugfix release. One of the bug fixes is particularly important if using -elliptic curve signatures. +elliptic curve signatures, please see below. #### Elliptic Curve Signature Length Bug Fix From c86c775caf411385f1fb8b98340008baed93419d Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:37:08 -0700 Subject: [PATCH 33/36] [maven-release-plugin] prepare release 0.7.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fc186277..7b2e40a6 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.7.0-SNAPSHOT + 0.7.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.7.0 GitHub Issues From 29241c3b6649314e9fff99b2c95e8deeecc19e38 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:37:12 -0700 Subject: [PATCH 34/36] [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 7b2e40a6..f62505fa 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ io.jsonwebtoken jjwt - 0.7.0 + 0.8.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.7.0 + HEAD GitHub Issues From 8966c3a912a923a995aaab7f056e0e709c5496af Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Mon, 12 Sep 2016 17:50:24 -0700 Subject: [PATCH 35/36] Added minor update to jackson version docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e16008a..ae8d547a 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,13 @@ These feature sets will be implemented in a future release. Community contribut #### Already using an older Jackson dependency? -JJWT depends on Jackson 2.4.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: +JJWT depends on Jackson 2.8.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see [runtime errors](https://github.com/jwtk/jjwt/issues/1). To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example: ```xml com.fasterxml.jackson.core jackson-databind - 2.4.2 + 2.8.2 ``` From 13906d3746d29e9610a0f3c385dbb14932fe9d71 Mon Sep 17 00:00:00 2001 From: sainaen Date: Tue, 20 Sep 2016 12:14:24 +0300 Subject: [PATCH 36/36] Implement type conversions of integral claim values Jackson chooses the target type for JSON numbers based on their value, while deserializing without correct typing information present. This leads to a confusing behavior: String token = Jwts.builder() .claim("byte", (byte) 42) .claim("short", (short) 42) .claim("int", 42) .claim("long_small", (long) 42) .claim("long_big", ((long) Integer.MAX_VALUE) + 42) .compact(); Claims claims = (Claims) Jwts.parser().parse(token).getBody(); claims.get("int", Integer.class); // => 42 claims.get("long_big", Long.class); // => ((long) Integer.MAX_VALUE) + 42 claims.get("long_small", Long.class); // throws RequiredTypeException: required=Long, found=Integer claims.get("short", Short.class); // throws RequiredTypeException: required=Short, found=Integer claims.get("byte", Byte.class); // throws RequiredTypeException: required=Byte, found=Integer With this commit, `DefaultClaims.getClaim(String, Class)` will correctly handle cases when required type is `Long`, `Integer`, `Short` or `Byte`: check that value fits in the required type and cast to it. // ... setup is the same as above claims.get("int", Integer.class); // => 42 claims.get("long_big", Long.class); // => ((long) Integer.MAX_VALUE) + 42 claims.get("long_small", Long.class); // => (long) 42 claims.get("short", Short.class); // => (short) 42 claims.get("byte", Byte.class); // => (byte) 42 Fixes #142. --- .../io/jsonwebtoken/impl/DefaultClaims.java | 15 +++ .../io/jsonwebtoken/JwtParserTest.groovy | 31 ++++++ .../impl/DefaultClaimsTest.groovy | 98 ++++++++++++++++++- 3 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 196c82ff..2523d305 100644 --- a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -120,10 +120,25 @@ public class DefaultClaims extends JwtMap implements Claims { value = getDate(claimName); } + return castClaimValue(value, requiredType); + } + + private T castClaimValue(Object value, Class requiredType) { if (requiredType == Date.class && value instanceof Long) { value = new Date((Long)value); } + if (value instanceof Integer) { + int intValue = (Integer) value; + if (requiredType == Long.class) { + value = (long) intValue; + } else if (requiredType == Short.class && Short.MIN_VALUE <= intValue && intValue <= Short.MAX_VALUE) { + value = (short) intValue; + } else if (requiredType == Byte.class && Byte.MIN_VALUE <= intValue && intValue <= Byte.MAX_VALUE) { + value = (byte) intValue; + } + } + if (!requiredType.isInstance(value)) { throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass()); } diff --git a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index f3a0368d..187711fe 100644 --- a/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -716,6 +716,37 @@ class JwtParserTest { } } + @Test + void testParseClaimsJwsWithNumericTypes() { + byte[] key = randomKey() + + def b = (byte) 42 + def s = (short) 42 + def i = 42 + + def smallLong = (long) 42 + def bigLong = ((long) Integer.MAX_VALUE) + 42 + + String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). + claim("byte", b). + claim("short", s). + claim("int", i). + claim("long_small", smallLong). + claim("long_big", bigLong). + compact() + + Jwt jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) + + Claims claims = jwt.getBody() + + assertEquals(b, claims.get("byte", Byte.class)) + assertEquals(s, claims.get("short", Short.class)) + assertEquals(i, claims.get("int", Integer.class)) + assertEquals(smallLong, claims.get("long_small", Long.class)) + assertEquals(bigLong, claims.get("long_big", Long.class)) + } + + // ======================================================================== // parsePlaintextJws with signingKey resolver. // ======================================================================== diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index 161957f0..4d642322 100644 --- a/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -52,11 +52,103 @@ class DefaultClaimsTest { } @Test - void testGetClaimWithRequiredType_Success() { - claims.put("anInteger", new Integer(5)) + void testGetClaimWithRequiredType_Integer_Success() { + def expected = new Integer(5) + claims.put("anInteger", expected) Object result = claims.get("anInteger", Integer.class) + assertEquals(expected, result) + } - assertTrue(result instanceof Integer) + @Test + void testGetClaimWithRequiredType_Long_Success() { + def expected = new Long(123) + claims.put("aLong", expected) + Object result = claims.get("aLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_LongWithInteger_Success() { + // long value that fits inside an Integer + def expected = new Long(Integer.MAX_VALUE - 100) + // deserialized as an Integer from JSON + // (type information is not available during parsing) + claims.put("smallLong", expected.intValue()) + // should still be available as Long + Object result = claims.get("smallLong", Long.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithInteger_Success() { + def expected = new Short((short) 42) + claims.put("short", expected.intValue()) + Object result = claims.get("short", Short.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ShortWithBigInteger_Exception() { + claims.put("tooBigForShort", ((int) Short.MAX_VALUE) + 42) + try { + claims.get("tooBigForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ShortWithSmallInteger_Exception() { + claims.put("tooSmallForShort", ((int) Short.MIN_VALUE) - 42) + try { + claims.get("tooSmallForShort", Short.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Short, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithInteger_Success() { + def expected = new Byte((byte) 42) + claims.put("byte", expected.intValue()) + Object result = claims.get("byte", Byte.class) + assertEquals(expected, result) + } + + @Test + void testGetClaimWithRequiredType_ByteWithBigInteger_Exception() { + claims.put("tooBigForByte", ((int) Byte.MAX_VALUE) + 42) + try { + claims.get("tooBigForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } + } + + @Test + void testGetClaimWithRequiredType_ByteWithSmallInteger_Exception() { + claims.put("tooSmallForByte", ((int) Byte.MIN_VALUE) - 42) + try { + claims.get("tooSmallForByte", Byte.class) + fail("getClaim() shouldn't silently lose precision.") + } catch (RequiredTypeException e) { + assertEquals( + e.getMessage(), + "Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer" + ) + } } @Test