From e30e584358b23f950c3b96cfc76830f80c74d254 Mon Sep 17 00:00:00 2001 From: James Bench Date: Wed, 27 Sep 2017 16:55:03 +0100 Subject: [PATCH 001/293] Add an interface for JWTVerifier. --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 3 ++- .../main/java/com/auth0/jwt/interfaces/JWTVerifier.java | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 6f513c7e..7e5ffed9 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -14,7 +14,7 @@ * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches. */ @SuppressWarnings("WeakerAccess") -public final class JWTVerifier { +public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { private final Algorithm algorithm; final Map claims; private final Clock clock; @@ -348,6 +348,7 @@ private void requireClaim(String name, Object value) { * @throws TokenExpiredException if the token has expired. * @throws InvalidClaimException if a claim contained a different value than the expected one. */ + @Override public DecodedJWT verify(String token) throws JWTVerificationException { DecodedJWT jwt = JWT.decode(token); verifyAlgorithm(jwt, algorithm); diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java new file mode 100644 index 00000000..49f285aa --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.interfaces; + +import com.auth0.jwt.exceptions.JWTVerificationException; + +public interface JWTVerifier { + DecodedJWT verify(String token) throws JWTVerificationException; +} From ed94ed499f28e60b16d285370938a3356f599ac2 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 21 Feb 2018 16:48:43 -0300 Subject: [PATCH 002/293] throw JWTDecodeException when date claim format is invalid --- .../main/java/com/auth0/jwt/impl/PayloadDeserializer.java | 5 ++++- lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java | 2 +- .../java/com/auth0/jwt/impl/PayloadDeserializerTest.java | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 38068872..8f872cb4 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -65,9 +65,12 @@ List getStringOrArray(Map tree, String claimName) thro Date getDateFromSeconds(Map tree, String claimName) { JsonNode node = tree.get(claimName); - if (node == null || node.isNull() || !node.canConvertToLong()) { + if (node == null || node.isNull()) { return null; } + if (!node.canConvertToLong()) { + throw new JWTDecodeException(String.format("The claim '%s' contained an unexpected value.", claimName)); + } final long ms = node.asLong() * 1000; return new Date(ms); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index a482685e..e164dc07 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -275,7 +275,7 @@ public void shouldGetCustomMapClaim() throws Exception { @Test public void shouldGetAvailableClaims() throws Exception { - DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxMjM0NTY3ODkwIiwiaWF0IjoiMTIzNDU2Nzg5MCIsIm5iZiI6IjEyMzQ1Njc4OTAiLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.TX9Ct4feGp9YyeGK9Zl91tO0YBOrguJ4As9jeqgHdZQ"); + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwibmJmIjoxMjM0NTY3ODkwLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.2_0nxDPJwOk64U5V5V9pt8U92jTPJbGsHYQ35HYhbdE"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaims(), is(notNullValue())); assertThat(jwt.getClaims(), is(instanceOf(Map.class))); diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java index 014ae838..7fe9145c 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -198,13 +198,15 @@ public void shouldGetNullDateWhenParsingNull() throws Exception { } @Test - public void shouldGetNullDateWhenParsingNonNumericNode() throws Exception { + public void shouldThrowWhenParsingNonNumericNode() throws Exception { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The claim 'key' contained an unexpected value."); + Map tree = new HashMap<>(); TextNode node = new TextNode("123456789"); tree.put("key", node); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(nullValue())); + deserializer.getDateFromSeconds(tree, "key"); } @Test From 95e7e517c1027af336ac2933acd8e6e3c4b397e6 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 29 May 2018 12:07:49 -0300 Subject: [PATCH 003/293] change exception message --- lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java | 2 +- .../test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 8f872cb4..43047bfc 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -69,7 +69,7 @@ Date getDateFromSeconds(Map tree, String claimName) { return null; } if (!node.canConvertToLong()) { - throw new JWTDecodeException(String.format("The claim '%s' contained an unexpected value.", claimName)); + throw new JWTDecodeException(String.format("The claim '%s' contained a non-numeric date value.", claimName)); } final long ms = node.asLong() * 1000; return new Date(ms); diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java index 7fe9145c..2e2cabed 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -200,7 +200,7 @@ public void shouldGetNullDateWhenParsingNull() throws Exception { @Test public void shouldThrowWhenParsingNonNumericNode() throws Exception { exception.expect(JWTDecodeException.class); - exception.expectMessage("The claim 'key' contained an unexpected value."); + exception.expectMessage("The claim 'key' contained a non-numeric date value."); Map tree = new HashMap<>(); TextNode node = new TextNode("123456789"); From 00132e97fd7e4cab8a7c5f4fc943168edebbce3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Mon, 26 Feb 2018 15:02:11 +0100 Subject: [PATCH 004/293] Bump Jackson dependency, add OWASP dependency-check plugin --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index fb782d71..377d4c85 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.2' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.5' compile 'commons-codec:commons-codec:1.11' testCompile 'org.bouncycastle:bcprov-jdk15on:1.58' testCompile 'junit:junit:4.12' From 9b3445375f7f998984d84f4107def619e2656584 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 13 Jun 2018 17:55:48 -0300 Subject: [PATCH 005/293] use jackson-databind 2.9.6 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 377d4c85..5e8f7059 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.5' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' compile 'commons-codec:commons-codec:1.11' testCompile 'org.bouncycastle:bcprov-jdk15on:1.58' testCompile 'junit:junit:4.12' From e7ff985d5199575c9594d2b1d627b096e6f8a87d Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 13 Jun 2018 17:47:10 -0300 Subject: [PATCH 006/293] bump dependencies. Fix security issue: Deserialization of Untrusted Data --- build.gradle | 3 +-- lib/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index b18f7596..f1dc45a3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.1' } } diff --git a/lib/build.gradle b/lib/build.gradle index 5e8f7059..7da318e5 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -36,11 +36,11 @@ compileJava { dependencies { compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' compile 'commons-codec:commons-codec:1.11' - testCompile 'org.bouncycastle:bcprov-jdk15on:1.58' + testCompile 'org.bouncycastle:bcprov-jdk15on:1.59' testCompile 'junit:junit:4.12' testCompile 'net.jodah:concurrentunit:0.4.3' testCompile 'org.hamcrest:java-hamcrest:2.0.0.0' - testCompile 'org.mockito:mockito-core:2.11.0' + testCompile 'org.mockito:mockito-core:2.18.3' } jacocoTestReport { From 8930e462b5fafd7e90dab35f3d7a0338c783e1fc Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 13 Jun 2018 18:13:05 -0300 Subject: [PATCH 007/293] Release 3.4.0 --- CHANGELOG.md | 10 ++++++++++ README.md | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb8210b..ba66c602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [3.4.0](https://github.com/auth0/java-jwt/tree/3.4.0) (2018-06-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.3.0...3.4.0) + +**Changed** +- Fix for issue #236 - refactored HMACAlgorithm so that it doesn't throw an UnsupportedEncodingException [\#242](https://github.com/auth0/java-jwt/pull/242) ([obecker](https://github.com/obecker)) +- Throw JWTDecodeException when date claim format is invalid [\#241](https://github.com/auth0/java-jwt/pull/241) ([lbalmaceda](https://github.com/lbalmaceda)) + +**Security** +- Bump Jackson dependency [\#244](https://github.com/auth0/java-jwt/pull/244) ([skjolber](https://github.com/skjolber)) + ## [3.3.0](https://github.com/auth0/java-jwt/tree/3.3.0) (2017-11-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.2.0...3.3.0) **Closed issues** diff --git a/README.md b/README.md index e98f1f79..39045fe9 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.3.0 + 3.4.0 ``` ### Gradle ```gradle -compile 'com.auth0:java-jwt:3.3.0' +compile 'com.auth0:java-jwt:3.4.0' ``` ## Available Algorithms From 1578d706f2057f89c76cac2ed4d51e0f9136e69e Mon Sep 17 00:00:00 2001 From: Andreas Gebhardt Date: Mon, 30 Jul 2018 20:03:30 +0200 Subject: [PATCH 008/293] =?UTF-8?q?update=20link=20title=20for=20=C2=BBJSO?= =?UTF-8?q?N=20Web=20Token=C2=AB=20(#217)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update link title for »JSON Web Token« to corresponding RFC title. Also update link target to Internet Engineering Task Force (IETF). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39045fe9..4cd787e1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt/v3.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) -A Java implementation of [JSON Web Tokens (draft-ietf-oauth-json-web-token-08)](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html). +A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. From 1e3696fd7558e4adcc2d77194b1b7f2ead4754a8 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 14 Aug 2018 10:03:51 -0300 Subject: [PATCH 009/293] remove jdk7 build --- .circleci/config.yml | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 909f794c..f8394fc2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,33 +1,10 @@ version: 2 jobs: - java_7_build: - docker: - - image: openjdk:7u121-jdk - steps: - - checkout - - run: chmod +x gradlew - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - # run tests! - - run: ./gradlew clean check jacocoTestReport --continue --console=plain - - run: - name: Upload Coverage - when: on_success - command: bash <(curl -s https://codecov.io/bash) - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} + build: environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb - - java_8_build: docker: - image: openjdk:8-jdk steps: @@ -48,15 +25,4 @@ jobs: - save_cache: paths: - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb - -workflows: - version: 2 - build: - jobs: - - java_7_build - - java_8_build + key: v1-dependencies-{{ checksum "build.gradle" }} \ No newline at end of file From edeb590441f5400b6e7a11c29a5dd821edc9c617 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 14 Aug 2018 11:50:52 -0300 Subject: [PATCH 010/293] update codecov patch settings --- .codecov.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index 63e5785f..51f857af 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,7 +5,8 @@ coverage: status: patch: default: - if_no_uploads: error + if_no_uploads: success + if_ci_failed: error changes: true project: default: From de3b80d0002b18f6f3736c3703446b7fe5b95033 Mon Sep 17 00:00:00 2001 From: Quynh Anh Nguyen Date: Thu, 4 Oct 2018 16:26:35 +0800 Subject: [PATCH 011/293] Improve README for the Using KeyProvider example --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 4cd787e1..6e92f8aa 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ By using a `KeyProvider` you can change in runtime the key used either to verify - `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. -The following snippet uses example classes showing how this would work: - +The following example shows how this would work with `JwkStore`, an imaginary [JWK Set](https://auth0.com/docs/jwks) implementation. For simple key rotation using JWKS, try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. ```java final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}"); @@ -105,9 +104,6 @@ Algorithm algorithm = Algorithm.RSA256(keyProvider); //Use the Algorithm to create and verify JWTs. ``` -> For simple key rotation using JWKs try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. - - ### Create and Sign a Token You'll first need to create a `JWTCreator` instance by calling `JWT.create()`. Use the builder to define the custom Claims your token needs to have. Finally to get the String token call `sign()` and pass the `Algorithm` instance. From ab1433e2bc117e7ae68aaf9a72bb93796750ac05 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 24 Oct 2018 13:58:08 -0300 Subject: [PATCH 012/293] stop building CI for JDK 7 --- .circleci/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f8394fc2..9bb01639 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,6 @@ version: 2 jobs: build: - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb docker: - image: openjdk:8-jdk steps: @@ -21,8 +17,12 @@ jobs: - run: name: Upload Coverage when: on_success - command: bash <(curl -s https://codecov.io/bash) + command: bash <(curl -s https://codecov.io/bash) -Z -C $CIRCLE_SHA1 - save_cache: paths: - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} \ No newline at end of file + key: v1-dependencies-{{ checksum "build.gradle" }} + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + TERM: dumb From d88136ee45e1030e285b46c0bc73ea052ea669d4 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 24 Oct 2018 13:52:43 -0300 Subject: [PATCH 013/293] update jackson-databind dependency --- .gitignore | 1 + lib/build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ab35b855..5ab9b34a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Temporary Items ## Plugin-specific files: # IntelliJ +bin/ /out/ /lib/out/ diff --git a/lib/build.gradle b/lib/build.gradle index 7da318e5..d6787ed4 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.6' + compile 'com.fasterxml.jackson.core:jackson-databind:2.9.7' compile 'commons-codec:commons-codec:1.11' testCompile 'org.bouncycastle:bcprov-jdk15on:1.59' testCompile 'junit:junit:4.12' From 09e5178f487330f3d00393405e5732befbc8522d Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 24 Oct 2018 14:06:41 -0300 Subject: [PATCH 014/293] Release 3.4.1 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba66c602..e4e454b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.4.1](https://github.com/auth0/java-jwt/tree/3.4.1) (2018-10-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.0...3.4.1) + +**Security** +- Update jackson-databind dependency [\#292](https://github.com/auth0/java-jwt/pull/292) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [3.4.0](https://github.com/auth0/java-jwt/tree/3.4.0) (2018-06-13) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.3.0...3.4.0) diff --git a/README.md b/README.md index 4cd787e1..53663e73 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.4.0 + 3.4.1 ``` ### Gradle ```gradle -compile 'com.auth0:java-jwt:3.4.0' +compile 'com.auth0:java-jwt:3.4.1' ``` ## Available Algorithms From aa01becf8e6c034dcab58c042c95761d8f800b42 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 24 Oct 2018 14:25:38 -0300 Subject: [PATCH 015/293] use latest OSS plugin. bumps gradle version too --- build.gradle | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 72 ++++++---- gradlew.bat | 14 +- lib/build.gradle | 62 ++++----- scripts/bintray.gradle | 55 -------- scripts/maven.gradle | 105 -------------- scripts/release.gradle | 170 ----------------------- 9 files changed, 79 insertions(+), 409 deletions(-) delete mode 100644 scripts/bintray.gradle delete mode 100644 scripts/maven.gradle delete mode 100644 scripts/release.gradle diff --git a/build.gradle b/build.gradle index f1dc45a3..c348b47a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,13 @@ buildscript { repositories { - jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } } dependencies { - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.1' + classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4" + classpath "gradle.plugin.com.auth0.gradle:oss-library:0.8.0" } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKzPVt 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ⅅ8zCNKueM5c!$)^mKpZNR!eIlFST57ePGQcqCqedAQ3UaUEzpjM--5V4YO zY22VxQm%$2NDnwfK+jkz=i2>NjAM6&P1DdcO<*Xs1-lzdXWn#LGSxwhPH7N%D8-zCgpFWt@`LgNYI+Fh^~nSiQmwH0^>E>*O$47MqfQza@Ce z1wBw;igLc#V2@y-*~Hp?jA1)+MYYyAt|DV_8RQCrRY@sAviO}wv;3gFdO>TE(=9o? z=S(r=0oT`w24=ihA=~iFV5z$ZG74?rmYn#eanx(!Hkxcr$*^KRFJKYYB&l6$WVsJ^ z-Iz#HYmE)Da@&seqG1fXsTER#adA&OrD2-T(z}Cwby|mQf{0v*v3hq~pzF`U`jenT z=XHXeB|fa?Ws$+9ADO0rco{#~+`VM?IXg7N>M0w1fyW1iiKTA@p$y zSiAJ%-Mg{m>&S4r#Tw@?@7ck}#oFo-iZJCWc`hw_J$=rw?omE{^tc59ftd`xq?jzf zo0bFUI=$>O!45{!c4?0KsJmZ#$vuYpZLo_O^oHTmmLMm0J_a{Nn`q5tG1m=0ecv$T z5H7r0DZGl6be@aJ+;26EGw9JENj0oJ5K0=^f-yBW2I0jqVIU};NBp*gF7_KlQnhB6 z##d$H({^HXj@il`*4^kC42&3)(A|tuhs;LygA-EWFSqpe+%#?6HG6}mE215Z4mjO2 zY2^?5$<8&k`O~#~sSc5Fy`5hg5#e{kG>SAbTxCh{y32fHkNryU_c0_6h&$zbWc63T z7|r?X7_H!9XK!HfZ+r?FvBQ$x{HTGS=1VN<>Ss-7M3z|vQG|N}Frv{h-q623@Jz*@ ziXlZIpAuY^RPlu&=nO)pFhML5=ut~&zWDSsn%>mv)!P1|^M!d5AwmSPIckoY|0u9I zTDAzG*U&5SPf+@c_tE_I!~Npfi$?gX(kn=zZd|tUZ_ez(xP+)xS!8=k(<{9@<+EUx zYQgZhjn(0qA#?~Q+EA9oh_Jx5PMfE3#KIh#*cFIFQGi)-40NHbJO&%ZvL|LAqU=Rw zf?Vr4qkUcKtLr^g-6*N-tfk+v8@#Lpl~SgKyH!+m9?T8B>WDWK22;!i5&_N=%f{__ z-LHb`v-LvKqTJZCx~z|Yg;U_f)VZu~q7trb%C6fOKs#eJosw&b$nmwGwP;Bz`=zK4 z>U3;}T_ptP)w=vJaL8EhW;J#SHA;fr13f=r#{o)`dRMOs-T;lp&Toi@u^oB_^pw=P zp#8Geo2?@!h2EYHY?L;ayT}-Df0?TeUCe8Cto{W0_a>!7Gxmi5G-nIIS;X{flm2De z{SjFG%knZoVa;mtHR_`*6)KEf=dvOT3OgT7C7&-4P#4X^B%VI&_57cBbli()(%zZC?Y0b;?5!f22UleQ=9h4_LkcA!Xsqx@q{ko&tvP_V@7epFs}AIpM{g??PA>U(sk$Gum>2Eu zD{Oy{$OF%~?B6>ixQeK9I}!$O0!T3#Ir8MW)j2V*qyJ z8Bg17L`rg^B_#rkny-=<3fr}Y42+x0@q6POk$H^*p3~Dc@5uYTQ$pfaRnIT}Wxb;- zl!@kkZkS=l)&=y|21veY8yz$t-&7ecA)TR|=51BKh(@n|d$EN>18)9kSQ|GqP?aeM ztXd9C&Md$PPF*FVs*GhoHM2L@D$(Qf%%x zwQBUt!jM~GgwluBcwkgwQ!249uPkNz3u@LSYZgmpHgX|P#8!iKk^vSKZ;?)KE$92d z2U>y}VWJ0&zjrIqddM3dz-nU%>bL&KU%SA|LiiUU7Ka|c=jF|vQ1V)Jz`JZe*j<5U6~RVuBEVJoY~ z&GE+F$f>4lN=X4-|9v*5O*Os>>r87u z!_1NSV?_X&HeFR1fOFb8_P)4lybJ6?1BWK`Tv2;4t|x1<#@17UO|hLGnrB%nu)fDk zfstJ4{X4^Y<8Lj<}g2^kksSefQTMuTo?tJLCh zC~>CR#a0hADw!_Vg*5fJwV{~S(j8)~sn>Oyt(ud2$1YfGck77}xN@3U_#T`q)f9!2 zf>Ia;Gwp2_C>WokU%(z2ec8z94pZyhaK+e>3a9sj^-&*V494;p9-xk+u1Jn#N_&xs z59OI2w=PuTErv|aNcK*>3l^W*p3}fjXJjJAXtBA#%B(-0--s;1U#f8gFYW!JL+iVG zV0SSx5w8eVgE?3Sg@eQv)=x<+-JgpVixZQNaZr}3b8sVyVs$@ndkF5FYKka@b+YAh z#nq_gzlIDKEs_i}H4f)(VQ!FSB}j>5znkVD&W0bOA{UZ7h!(FXrBbtdGA|PE1db>s z$!X)WY)u#7P8>^7Pjjj-kXNBuJX3(pJVetTZRNOnR5|RT5D>xmwxhAn)9KF3J05J; z-Mfb~dc?LUGqozC2p!1VjRqUwwDBnJhOua3vCCB-%ykW_ohSe?$R#dz%@Gym-8-RA zjMa_SJSzIl8{9dV+&63e9$4;{=1}w2=l+_j_Dtt@<(SYMbV-18&%F@Zl7F_5! z@xwJ0wiDdO%{}j9PW1(t+8P7Ud79yjY>x>aZYWJL_NI?bI6Y02`;@?qPz_PRqz(7v``20`- z033Dy|4;y6di|>cz|P-z|6c&3f&g^OAt8aN0Zd&0yZ>dq2aFCsE<~Ucf$v{sL=*++ zBxFSa2lfA+Y%U@B&3D=&CBO&u`#*nNc|PCY7XO<}MnG0VR764XrHtrb5zwC*2F!Lp zE<~Vj0;z!S-|3M4DFxuQ=`ShTf28<9p!81(0hFbGNqF%0gg*orez9!qt8e%o@Yfl@ zhvY}{@3&f??}7<`p>FyU;7?VkKbh8_=csozU=|fH&szgZ{=NDCylQ>EH^x5!K3~-V z)_2Y>0uJ`Z0Pb58y`RL+&n@m9tJ)O<%q#&u#DAIt+-rRt0eSe1MTtMl@W)H$b3D)@ z*A-1bUgZI)>HdcI4&W>P4W5{-j=s5p5`cbQ+{(g0+RDnz!TR^mxSLu_y#SDVKrj8i zA^hi6>jMGM;`$9Vfb-Yf!47b)Ow`2OKtNB=z|Kxa$5O}WPo;(Dc^`q(7X8kkeFyO8 z{XOq^07=u|7*P2`m;>PIFf=i80MKUxsN{d2cX0M+REsE*20+WQ79T9&cqT>=I_U% z{=8~^Isg(Nzo~`4iQfIb_#CVCD>#5h>=-Z#5dH}WxYzn%0)GAm6L2WdUdP=0_h>7f z(jh&7%1i(ZOn+}D8$iGK4Vs{pmHl_w4Qm-46H9>4^{3dz^DZDh+dw)6Xd@CpQNK$j z{CU;-cmpK=egplZ3y3%y=sEnCJ^eYVKXzV8H2_r*fJ*%*B;a1_lOpt6)IT1IAK2eB z{rie|uDJUrbgfUE>~C>@RO|m5ex55F{=~Bb4Cucp{ok7Yf9V}QuZ`#Gc|WaqsQlK- zKaV)iMRR__&Ak2Z=IM9R9g5$WM4u{a^C-7uX*!myEym z#_#p^T!P~#Dx$%^K>Y_nj_3J*E_LwJ60-5Xu=LkJAwcP@|0;a&+|+ZX`Jbj9P5;T% z|KOc}4*#4o{U?09`9Hz`Xo-I!P=9XfIrr*MQ}y=$!qgv?_J38^bNb4kM&_OVg^_=Eu-qG5U(fw0KMgH){C8pazq~51rN97hf#20-7=aK0)N|UM H-+%o-(+5aQ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 04e285f3..d76b502e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Dec 28 10:00:20 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew index 9d82f789..cccdd3d5 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..e95643d6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/lib/build.gradle b/lib/build.gradle index d6787ed4..f8a85c50 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,30 +1,27 @@ -apply plugin: 'jacoco' -apply plugin: 'java' -apply from: '../scripts/release.gradle' -apply from: '../scripts/maven.gradle' -apply from: '../scripts/bintray.gradle' +apply plugin: "com.auth0.gradle.oss-library.java" +apply plugin: "jacoco" logger.lifecycle("Using version ${version} for ${group}.${name}") -auth0 { +oss { name "java jwt" - repo "java-jwt" + repository "java-jwt" + organization "auth0" description "Java implementation of JSON Web Token (JWT)" - url 'http://www.jwt.io' - developer { - id = "auth0" - name = "Auth0" - email = "oss@auth0.com" - } - developer { - id = "lbalmaceda" - name = "Luciano Balmaceda" - email = "luciano.balmaceda@auth0.com" - } - developer { - id = "hzalaz" - name = "Hernan Zalazar" - email = "hernan@auth0.com" + + developers { + auth0 { + displayName = "Auth0" + email = "oss@auth0.com" + } + lbalmaceda { + displayName = "Luciano Balmaceda" + email = "luciano.balmaceda@auth0.com" + } + hzalaz { + displayName = "Hernan Zalazar" + email = "hernan@auth0.com" + } } } @@ -34,13 +31,13 @@ compileJava { } dependencies { - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.7' - compile 'commons-codec:commons-codec:1.11' - testCompile 'org.bouncycastle:bcprov-jdk15on:1.59' - testCompile 'junit:junit:4.12' - testCompile 'net.jodah:concurrentunit:0.4.3' - testCompile 'org.hamcrest:java-hamcrest:2.0.0.0' - testCompile 'org.mockito:mockito-core:2.18.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.7' + implementation 'commons-codec:commons-codec:1.11' + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59' + testImplementation 'junit:junit:4.12' + testImplementation 'net.jodah:concurrentunit:0.4.3' + testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' + testImplementation 'org.mockito:mockito-core:2.18.3' } jacocoTestReport { @@ -55,9 +52,4 @@ test { events "skipped", "failed", "standardError" exceptionFormat "short" } -} - -task clean(type: Delete) { - delete rootProject.buildDir - delete 'CHANGELOG.md.release' -} +} \ No newline at end of file diff --git a/scripts/bintray.gradle b/scripts/bintray.gradle deleted file mode 100644 index ba868123..00000000 --- a/scripts/bintray.gradle +++ /dev/null @@ -1,55 +0,0 @@ -def credentials = new Bintray(project); - -if (credentials.valid()) { - apply plugin: 'com.jfrog.bintray' - bintray { - user = credentials.user - key = credentials.key - publications = ['mavenJava'] - dryRun = project.version.endsWith("-SNAPSHOT") - publish = false - pkg { - repo = 'java' - name = 'java-jwt' - desc = 'Java implementation of JSON Web Token (JWT) ' - websiteUrl = 'https://github.com/auth0/java-jwt' - vcsUrl = 'scm:git@github.com:auth0/java-jwt.git' - licenses = ["MIT"] - userOrg = 'auth0' - publish = false - version { - gpg { - sign = true - passphrase = credentials.passphrasse - } - vcsTag = project.version - name = project.version - released = new Date() - } - } - } -} - -class Bintray { - String user - String key - String passphrasse - - Bintray(project) { - this.user = Bintray.value(project, 'BINTRAY_USER', 'bintray.user') - this.key = Bintray.value(project, 'BINTRAY_KEY', 'bintray.key') - this.passphrasse = Bintray.value(project, 'BINTRAY_PASSPHRASE', 'bintray.gpg.password') - } - - def valid() { - return this.user != null && this.key != null && this.passphrasse != null; - } - - private static def value(Project project, String env, String property) { - def value = System.getenv(env) - if (project.hasProperty(property)) { - value = project.getProperty(property); - } - return value - } -} \ No newline at end of file diff --git a/scripts/maven.gradle b/scripts/maven.gradle deleted file mode 100644 index a541b008..00000000 --- a/scripts/maven.gradle +++ /dev/null @@ -1,105 +0,0 @@ -apply plugin: Auth0OSS - -class Auth0OSS implements Plugin { - - void apply(Project target) { - target.extensions.create("auth0", Auth0Extension, target) - target.configure(target) { - apply plugin: 'maven-publish' - - target.task("sourcesJar", type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - target.task("javadocJar", type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.getDestinationDir() - } - - artifacts { - archives sourcesJar, javadocJar - } - - publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar - groupId project.group - artifactId project.name - version project.version - } - } - } - - publishing.publications.all { - pom.withXml { - - def lib = project.extensions.auth0 - def root = asNode() - - root.appendNode('packaging', 'jar') - root.appendNode('name', lib.name) - root.appendNode('description', lib.description) - root.appendNode('url', lib.url) - - def developersNode = root.appendNode('developers') - project.extensions.auth0.developers.each { - def node = developersNode.appendNode('developer') - node.appendNode('id', it.id) - node.appendNode('name', it.name) - node.appendNode('email', it.email) - } - - def dependenciesNode = root.appendNode('dependencies') - - configurations.compile.allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - } - - def licenceNode = root.appendNode('licenses').appendNode('license') - licenceNode.appendNode('name', 'The MIT License (MIT)') - licenceNode.appendNode('url', "https://raw.githubusercontent.com/auth0/${lib.repo}/master/LICENSE") - licenceNode.appendNode('distribution', 'repo') - - def scmNode = root.appendNode('scm') - scmNode.appendNode('connection', "scm:git@github.com:auth0/${lib.repo}.git") - scmNode.appendNode('developerConnection', "scm:git@github.com:auth0/${lib.repo}.git") - scmNode.appendNode('url', "https://github.com/auth0/${lib.repo}") - } - } - } - } -} - -class Auth0Extension { - String name - String repo - String description - String url - List developers = [] - - private Project project - - Auth0Extension(project) { - this.project = project - } - - void developer(Closure developerClosure) { - def developer = project.configure(new Developer(), developerClosure) - developers.add(developer) - } -} - -class Developer { - String id - String name - String email -} - - - diff --git a/scripts/release.gradle b/scripts/release.gradle deleted file mode 100644 index da74e9b5..00000000 --- a/scripts/release.gradle +++ /dev/null @@ -1,170 +0,0 @@ -import java.text.SimpleDateFormat - -apply plugin: ReleasePlugin - -class Semver { - String version - def snapshot - - def getStringVersion() { - return snapshot ? "$version-SNAPSHOT" : version - } - - def nextPatch() { - def parts = version.split("\\.") - def patch = Integer.parseInt(parts[2]) + 1 - return "${parts[0]}.${parts[1]}.${patch}" - } - - def nextMinor() { - def parts = version.split("\\.") - def minor = Integer.parseInt(parts[1]) + 1 - return "${parts[0]}.${minor}.0" - } - -} - -class ChangeLogTask extends DefaultTask { - - def current - def next - - @TaskAction - def update() { - def repository = project.auth0.repo - def file = new File('CHANGELOG.md') - def output = new File('CHANGELOG.md.release') - output.newWriter().withWriter { writer -> - - file.eachLine { line, number -> - if (number == 0 && !line.startsWith('# Change Log')) { - throw new GradleException('Change Log file is not properly formatted') - } - - writer.println(line) - - if (number == 0 || number > 1) { - return - } - - def formatter = new SimpleDateFormat('yyyy-MM-dd') - writer.println() - writer.println("## [${next}](https://github.com/auth0/${repository}/tree/${next}) (${formatter.format(new Date())})") - writer.println("[Full Changelog](https://github.com/auth0/${repository}/compare/${current}...${next})") - def command = ["curl", "https://webtask.it.auth0.com/api/run/wt-hernan-auth0_com-0/oss-changelog.js?webtask_no_cache=1&repo=${repository}&milestone=${next}", "-f", "-s", "-H", "Accept: text/markdown"] - def content = command.execute() - content.consumeProcessOutputStream(writer) - if (content.waitFor() != 0) { - throw new GradleException("Failed to request changelog for version ${next}") - } - } - } - file.delete() - output.renameTo('CHANGELOG.md') - } -} - -class ReleaseTask extends DefaultTask { - - def tagName - - @TaskAction - def perform() { - def path = project.getRootProject().getProjectDir().path - project.exec { - commandLine 'git', 'add', 'README.md' - workingDir path - } - project.exec { - commandLine 'git', 'add', 'CHANGELOG.md' - workingDir path - } - project.exec { - commandLine 'git', 'commit', '-m', "Release ${tagName}" - workingDir path - } - project.exec { - commandLine 'git', 'tag', "${tagName}" - workingDir path - } - } -} - -class ReadmeTask extends DefaultTask { - - def current - def next - - final filename = 'README.md' - - @TaskAction - def update() { - def file = new File(filename) - def gradleUpdated = "compile '${project.group}:${project.name}:${next}'" - def oldSingleQuote = "compile '${project.group}:${project.name}:${current}'" - def oldDoubleQuote = "compile \"${project.group}:${project.name}:${current}\"" - def mavenUpdated = "${next}" - def mavenOld = "${current}" - def contents = file.getText('UTF-8') - contents = contents.replace(oldSingleQuote, gradleUpdated).replace(oldDoubleQuote, gradleUpdated).replace(mavenOld, mavenUpdated) - file.write(contents, 'UTF-8') - } - -} - -class ReleasePlugin implements Plugin { - void apply(Project target) { - def semver = current() - target.version = semver.stringVersion - def version = semver.version - def nextMinor = semver.nextMinor() - def nextPatch = semver.nextPatch() - target.task('changelogMinor', type: ChangeLogTask) { - current = version - next = nextMinor - } - target.task('changelogPatch', type: ChangeLogTask) { - current = version - next = nextPatch - } - target.task('readmeMinor', type: ReadmeTask, dependsOn: 'changelogMinor') { - current = version - next = nextMinor - } - target.task('readmePatch', type: ReadmeTask, dependsOn: 'changelogPatch') { - current = version - next = nextPatch - } - target.task('releaseMinor', type: ReleaseTask, dependsOn: 'readmeMinor') { - tagName = nextMinor - } - target.task('releasePatch', type: ReleaseTask, dependsOn: 'readmePatch') { - tagName = nextPatch - } - } - - static def current() { - def current = describeGit(false) - def snapshot = current == null - if (snapshot) { - current = describeGit(snapshot, '0.0.1') - } - return new Semver(snapshot: snapshot, version: current) - } - - static def describeGit(boolean snapshot, String defaultValue = null) { - def command = ['git', 'describe', '--tags', (snapshot ? '--abbrev=0' : '--exact-match')].execute() - def stdout = new ByteArrayOutputStream() - def errout = new ByteArrayOutputStream() - command.consumeProcessOutput(stdout, errout) - if (command.waitFor() != 0) { - Logging.getLogger(ReleasePlugin.class).debug(errout.toString()) - return defaultValue - } - if (stdout.toByteArray().length > 0) { - return stdout.toString().replace('\n', "") - } - - return defaultValue - } -} \ No newline at end of file From 571f0a2627d181e135bf1b96c7809b38a2009f11 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 24 Oct 2018 14:31:52 -0300 Subject: [PATCH 016/293] use new plugin sintax --- build.gradle | 12 ------------ lib/build.gradle | 7 +++++-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index c348b47a..bf6dfe4f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - maven { - url "https://plugins.gradle.org/m2/" - } - } - dependencies { - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4" - classpath "gradle.plugin.com.auth0.gradle:oss-library:0.8.0" - } -} - allprojects { group = 'com.auth0' diff --git a/lib/build.gradle b/lib/build.gradle index f8a85c50..041b3472 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,8 @@ -apply plugin: "com.auth0.gradle.oss-library.java" -apply plugin: "jacoco" +plugins { + id "com.jfrog.bintray" version "1.8.4" + id "com.auth0.gradle.oss-library.java" version "0.8.0" + id "jacoco" +} logger.lifecycle("Using version ${version} for ${group}.${name}") From 92789bfaa8dd7d897a55af6290b08488706adf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Horstmann?= Date: Mon, 29 Oct 2018 14:49:18 +0100 Subject: [PATCH 017/293] Remove unnecessary cast between long/double and floor call Calculating `time / 1000 * 1000` already returns the correctly truncated result using long arithmetic --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 6f513c7e..30099252 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -427,7 +427,7 @@ private void assertValidStringClaim(String claimName, String value, String expec private void assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) { Date today = clock.getToday(); - today.setTime((long) Math.floor((today.getTime() / 1000) * 1000)); // truncate millis + today.setTime(today.getTime() / 1000 * 1000); // truncate millis if (shouldBeFuture) { assertDateIsFuture(date, leeway, today); } else { From 2d61d32011306c36cae11695872d77e3750f24d7 Mon Sep 17 00:00:00 2001 From: "veerasekharbab.golla" Date: Tue, 30 Oct 2018 14:21:07 +0530 Subject: [PATCH 018/293] Issue#254: iat validation will be done by default and it can be ignored with ignoreIssuedAt(). --- .../main/java/com/auth0/jwt/JWTVerifier.java | 27 +++++++++++-------- .../java/com/auth0/jwt/JWTCreatorTest.java | 1 + .../java/com/auth0/jwt/JWTVerifierTest.java | 27 +++++++++++++++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 6f513c7e..00f49921 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -43,6 +43,7 @@ public static class BaseVerification implements Verification { private final Algorithm algorithm; private final Map claims; private long defaultLeeway; + private boolean ignoreIssuedAt; BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { @@ -150,6 +151,10 @@ public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException return this; } + public void ignoreIssuedAt() { + this.ignoreIssuedAt = true; + } + /** * Require a specific JWT Id ("jti") claim. * @@ -316,17 +321,17 @@ private void assertNonNull(String name) { } } - private void addLeewayToDateClaims() { - if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { - claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { - claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); - } - } + private void addLeewayToDateClaims() { + if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { + claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); + } + if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { + claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); + } + if (!ignoreIssuedAt && !claims.containsKey(PublicClaims.ISSUED_AT)) { + claims.put(PublicClaims.ISSUED_AT, defaultLeeway); + } + } private void requireClaim(String name, Object value) { if (value == null) { diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index ab263bad..571be017 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -195,6 +195,7 @@ public void shouldAddIssuedAt() throws Exception { .withIssuedAt(new Date(1477592000)) .sign(Algorithm.HMAC256("secret")); + System.out.println(signed); assertThat(signed, is(notNullValue())); assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9")); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index d5826c24..459fa37d 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -494,6 +494,33 @@ public void shouldValidateIssuedAtWithLeeway() throws Exception { assertThat(jwt, is(notNullValue())); } + // Issued At with future date + @Test (expected = InvalidClaimException.class) + public void shouldThrowOnFutureIssuedAt() throws Exception { + Clock clock = mock(Clock.class); + when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + + DecodedJWT jwt = verification.build(clock).verify(token); + assertThat(jwt, is(notNullValue())); + } + + // Issued At with future date and ignore flag + @Test + public void shouldNotVerifyIATOnIgnoreIssuedAt() throws Exception { + Clock clock = mock(Clock.class); + when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification.ignoreIssuedAt(); + + DecodedJWT jwt = verification.build(clock).verify(token); + assertThat(jwt, is(notNullValue())); + } + @Test public void shouldThrowOnInvalidIssuedAtIfPresent() throws Exception { exception.expect(InvalidClaimException.class); From 15ab908ab1274391ddca74882a426ba862a8165e Mon Sep 17 00:00:00 2001 From: "veerasekharbab.golla" Date: Tue, 30 Oct 2018 14:32:55 +0530 Subject: [PATCH 019/293] ignoring the sysouts added while testing --- lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 571be017..ab263bad 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -195,7 +195,6 @@ public void shouldAddIssuedAt() throws Exception { .withIssuedAt(new Date(1477592000)) .sign(Algorithm.HMAC256("secret")); - System.out.println(signed); assertThat(signed, is(notNullValue())); assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9")); } From 0b02e98089828fe66e7d2bb4137de1977e6aa6d2 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 16 Nov 2018 15:48:11 -0300 Subject: [PATCH 020/293] Add missing javadoc --- .../main/java/com/auth0/jwt/interfaces/JWTVerifier.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index 49f285aa..1159c1fb 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -2,6 +2,15 @@ import com.auth0.jwt.exceptions.JWTVerificationException; + public interface JWTVerifier { + + /** + * Performs the verification against the given Token + * + * @param token to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ DecodedJWT verify(String token) throws JWTVerificationException; } From dc1a367319b53ac7053fbaf3ee837a3c61cfdd55 Mon Sep 17 00:00:00 2001 From: lostship <418181589@qq.com> Date: Tue, 25 Dec 2018 18:41:42 +0800 Subject: [PATCH 021/293] Update README.md Time Validation * The token can already be used. `"nbf" < TODAY` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49a5632e..51a968d3 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ If the token has an invalid signature or the Claim requirement is not met, a `JW The JWT token may include DateNumber fields that can be used to validate that: * The token was issued in a past date `"iat" < TODAY` * The token hasn't expired yet `"exp" > TODAY` and -* The token can already be used. `"nbf" > TODAY` +* The token can already be used. `"nbf" < TODAY` When verifying a token the time validation occurs automatically, resulting in a `JWTVerificationException` being throw when the values are invalid. If any of the previous fields are missing they won't be considered in this validation. From 7cfb8c5a73808c470d31e1cdc83a14c05f1580f5 Mon Sep 17 00:00:00 2001 From: Martin O'Connor Date: Fri, 28 Dec 2018 10:07:18 -0500 Subject: [PATCH 022/293] Verify a DecodedJWT. Allows verification of a DecodedJWT previously obtained by invoking JWT.decode(). This alleviates the user from having to recompute the values of the DecodedJWT in order to validate the token. Reopens #279 --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 15 +++++++++++++++ .../com/auth0/jwt/interfaces/JWTVerifier.java | 9 +++++++++ lib/src/test/java/com/auth0/jwt/JWTTest.java | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index afeed0eb..456fee11 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -351,6 +351,21 @@ private void requireClaim(String name, Object value) { @Override public DecodedJWT verify(String token) throws JWTVerificationException { DecodedJWT jwt = JWT.decode(token); + return verify(jwt); + } + + /** + * Perform the verification against the given decoded JWT, using any previous configured options. + * + * @param jwt to verify. + * @return a verified and decoded JWT. + * @throws AlgorithmMismatchException if the algorithm stated in the token's header it's not equal to the one defined in the {@link JWTVerifier}. + * @throws SignatureVerificationException if the signature is invalid. + * @throws TokenExpiredException if the token has expired. + * @throws InvalidClaimException if a claim contained a different value than the expected one. + */ + @Override + public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException { verifyAlgorithm(jwt, algorithm); algorithm.verify(jwt); verifyClaims(jwt, claims); diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index 1159c1fb..140af8e6 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -13,4 +13,13 @@ public interface JWTVerifier { * @throws JWTVerificationException if any of the verification steps fail */ DecodedJWT verify(String token) throws JWTVerificationException; + + /** + * Performs the verification against the given decoded JWT + * + * @param jwt to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ + DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException; } diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 31df4494..579ea271 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -58,6 +58,18 @@ public void shouldGetStringToken() throws Exception { // Verify + @Test + public void shouldVerifyDecodedToken() throws Exception { + String token = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; + DecodedJWT decodedJWT = JWT.decode(token); + RSAKey key = (RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"); + DecodedJWT jwt = JWT.require(Algorithm.RSA512(key)) + .build() + .verify(decodedJWT); + + assertThat(jwt, is(notNullValue())); + } + @Test public void shouldAcceptNoneAlgorithm() throws Exception { String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9."; From 326d58274e8de747aca5b38596464b48b8d8e3aa Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 3 Jan 2019 11:29:15 -0300 Subject: [PATCH 023/293] bump jackson dependency --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 041b3472..c13635e4 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.7' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' implementation 'commons-codec:commons-codec:1.11' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59' testImplementation 'junit:junit:4.12' From 24319b70eaf627982d41c129272d261abf37800c Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 3 Jan 2019 15:35:13 -0300 Subject: [PATCH 024/293] Release 3.5.0 --- CHANGELOG.md | 15 +++++++++++++++ README.md | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e454b9..6bce7fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Change Log +## [3.5.0](https://github.com/auth0/java-jwt/tree/3.5.0) (2019-01-03) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.1...3.5.0) + +**Added** +- Verify a DecodedJWT [\#308](https://github.com/auth0/java-jwt/pull/308) ([martinoconnor](https://github.com/martinoconnor)) + +**Changed** +- Add an interface for JWTVerifier. [\#205](https://github.com/auth0/java-jwt/pull/205) ([jebbench](https://github.com/jebbench)) + +**Fixed** +- Remove unnecessary cast between long/double and floor call [\#296](https://github.com/auth0/java-jwt/pull/296) ([jhorstmann](https://github.com/jhorstmann)) + +**Security** +- Bump jackson-databind to patch security issues [\#309](https://github.com/auth0/java-jwt/pull/309) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [3.4.1](https://github.com/auth0/java-jwt/tree/3.4.1) (2018-10-24) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.0...3.4.1) diff --git a/README.md b/README.md index 51a968d3..c9a08cd8 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.4.1 + 3.5.0 ``` ### Gradle ```gradle -compile 'com.auth0:java-jwt:3.4.1' +implementation 'com.auth0:java-jwt:3.5.0' ``` ## Available Algorithms From c9b6f296870e2293930e02ebca0bd2cfa9a83556 Mon Sep 17 00:00:00 2001 From: "veerasekharbab.golla" Date: Fri, 11 Jan 2019 15:29:58 +0530 Subject: [PATCH 025/293] Updated the code with review comments --- .../main/java/com/auth0/jwt/JWTVerifier.java | 15 +++-- .../auth0/jwt/interfaces/Verification.java | 2 + .../java/com/auth0/jwt/JWTVerifierTest.java | 64 +++++++++---------- 3 files changed, 44 insertions(+), 37 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 00f49921..320a39f1 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -151,9 +151,13 @@ public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException return this; } - public void ignoreIssuedAt() { - this.ignoreIssuedAt = true; - } + /** + * Call this method to skip the default issued_at verification + */ + public Verification ignoreIssuedAt() { + this.ignoreIssuedAt = true; + return this; + } /** * Require a specific JWT Id ("jti") claim. @@ -328,9 +332,12 @@ private void addLeewayToDateClaims() { if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); } - if (!ignoreIssuedAt && !claims.containsKey(PublicClaims.ISSUED_AT)) { + if (!claims.containsKey(PublicClaims.ISSUED_AT)) { claims.put(PublicClaims.ISSUED_AT, defaultLeeway); } + if(ignoreIssuedAt) { + claims.remove(PublicClaims.ISSUED_AT); + } } private void requireClaim(String name, Object value) { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 465ebe5d..a38468ab 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -37,5 +37,7 @@ public interface Verification { Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; + Verification ignoreIssuedAt(); + JWTVerifier build(); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 459fa37d..d7bcdd2c 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -478,48 +478,32 @@ public void shouldThrowOnNegativeNotBeforeLeeway() throws Exception { .acceptNotBefore(-1); } - // Issued At - @Test - public void shouldValidateIssuedAtWithLeeway() throws Exception { +// Issued At with future date + @Test (expected = InvalidClaimException.class) + public void shouldThrowOnFutureIssuedAt() throws Exception { Clock clock = mock(Clock.class); when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) - .acceptIssuedAt(2); - DecodedJWT jwt = verification - .build(clock) - .verify(token); + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification.build(clock).verify(token); assertThat(jwt, is(notNullValue())); } - // Issued At with future date - @Test (expected = InvalidClaimException.class) - public void shouldThrowOnFutureIssuedAt() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); - - String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - - DecodedJWT jwt = verification.build(clock).verify(token); - assertThat(jwt, is(notNullValue())); - } - - // Issued At with future date and ignore flag - @Test - public void shouldNotVerifyIATOnIgnoreIssuedAt() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + // Issued At with future date and ignore flag + @Test + public void shouldSkipIssuedAtVerificationWhenFlagIsPassed() throws Exception { + Clock clock = mock(Clock.class); + when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); - String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification.ignoreIssuedAt(); + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification.ignoreIssuedAt(); - DecodedJWT jwt = verification.build(clock).verify(token); - assertThat(jwt, is(notNullValue())); - } + DecodedJWT jwt = verification.build(clock).verify(token); + assertThat(jwt, is(notNullValue())); + } @Test public void shouldThrowOnInvalidIssuedAtIfPresent() throws Exception { @@ -535,6 +519,20 @@ public void shouldThrowOnInvalidIssuedAtIfPresent() throws Exception { .verify(token); } + @Test + public void shouldOverrideAcceptIssuedAtWhenIgnoreIssuedAtFlagPassedAndSkipTheVerification() throws Exception { + Clock clock = mock(Clock.class); + when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification.acceptIssuedAt(20).ignoreIssuedAt() + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + @Test public void shouldValidateIssuedAtIfPresent() throws Exception { Clock clock = mock(Clock.class); From 89400adbbb8b80dad66ada35cb44eb4b18a5952c Mon Sep 17 00:00:00 2001 From: "veerasekharbab.golla" Date: Sat, 12 Jan 2019 07:51:10 +0530 Subject: [PATCH 026/293] Formatter method and moved the ignoreIssuedAt to appropriate place --- .../main/java/com/auth0/jwt/JWTVerifier.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 002ce6ad..26de4034 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -152,7 +152,7 @@ public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException } /** - * Call this method to skip the default issued_at verification + * Skip the Issued At ("iat") date verification. By default, the verification is performed. */ public Verification ignoreIssuedAt() { this.ignoreIssuedAt = true; @@ -325,20 +325,21 @@ private void assertNonNull(String name) { } } - private void addLeewayToDateClaims() { - if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { - claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { - claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); - } - if(ignoreIssuedAt) { - claims.remove(PublicClaims.ISSUED_AT); - } - } + private void addLeewayToDateClaims() { + if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { + claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); + } + if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { + claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); + } + if(ignoreIssuedAt) { + claims.remove(PublicClaims.ISSUED_AT); + return; + } + if (!claims.containsKey(PublicClaims.ISSUED_AT)) { + claims.put(PublicClaims.ISSUED_AT, defaultLeeway); + } + } private void requireClaim(String name, Object value) { if (value == null) { From b4265aff17cc2ba234f9d64078b10c1a8206f416 Mon Sep 17 00:00:00 2001 From: Gabe Terrell Date: Mon, 21 Jan 2019 10:05:33 -0600 Subject: [PATCH 027/293] Bumping org.bouncycastle:bcprov-jdk15on version --- lib/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index c13635e4..7126e324 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -36,7 +36,7 @@ compileJava { dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' implementation 'commons-codec:commons-codec:1.11' - testImplementation 'org.bouncycastle:bcprov-jdk15on:1.59' + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' testImplementation 'net.jodah:concurrentunit:0.4.3' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' @@ -55,4 +55,4 @@ test { events "skipped", "failed", "standardError" exceptionFormat "short" } -} \ No newline at end of file +} From a55854fb00ef484dd1aeb78d0befa7a57e2768d0 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 24 Jan 2019 10:05:02 -0300 Subject: [PATCH 028/293] Release 3.6.0 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bce7fb8..9e0aef02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.6.0](https://github.com/auth0/java-jwt/tree/3.6.0) (2019-01-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.5.0...3.6.0) + +**Added** +- Allow to skip "issued at" validation [\#297](https://github.com/auth0/java-jwt/pull/297) ([complanboy2](https://github.com/complanboy2)) + ## [3.5.0](https://github.com/auth0/java-jwt/tree/3.5.0) (2019-01-03) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.1...3.5.0) diff --git a/README.md b/README.md index c9a08cd8..5b8c7e07 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.5.0 + 3.6.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.5.0' +implementation 'com.auth0:java-jwt:3.6.0' ``` ## Available Algorithms From 35b2ea7a4635eb874b553eff05259a6cf3f10f66 Mon Sep 17 00:00:00 2001 From: Maxim Balan Date: Thu, 24 Jan 2019 14:48:55 +0000 Subject: [PATCH 029/293] fixing JwtCreator to set the headers to the exisitng map instead of ovewriting it --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index a0889876..9c295f8d 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -71,7 +71,7 @@ public static class Builder { * @return this same Builder instance. */ public Builder withHeader(Map headerClaims) { - this.headerClaims = new HashMap<>(headerClaims); + this.headerClaims.putAll(headerClaims); return this; } From 9d94214275bfdb8eaf1a171fac03bd4557b407ea Mon Sep 17 00:00:00 2001 From: Maxim Balan Date: Thu, 24 Jan 2019 16:18:59 +0000 Subject: [PATCH 030/293] adding fails safty against empty map and removing a header if it was provided with a null value in the map also added unit test for this behaviour --- .../main/java/com/auth0/jwt/JWTCreator.java | 14 +++++- .../java/com/auth0/jwt/JWTCreatorTest.java | 43 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 9c295f8d..b656d5f8 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -66,12 +66,24 @@ public static class Builder { /** * Add specific Claims to set as the Header. + * If provided map is null then nothing + * If provided map contains a header with null value then that header will be removed from the header claims * * @param headerClaims the values to use as Claims in the token's Header. * @return this same Builder instance. */ public Builder withHeader(Map headerClaims) { - this.headerClaims.putAll(headerClaims); + if (headerClaims == null) + return this; + + for (Map.Entry entry : headerClaims.entrySet()) { + if (entry.getValue() == null) { + this.headerClaims.remove(entry.getKey()); + } else { + this.headerClaims.put(entry.getKey(), entry.getValue()); + } + } + return this; } diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index ab263bad..f2a54803 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -53,6 +53,49 @@ public void shouldAddHeaderClaim() throws Exception { assertThat(headerJson, JsonMatcher.hasEntry("asd", 123)); } + @Test + public void shouldReturnBuilderIfNullMapIsProvided() throws Exception { + String signed = JWTCreator.init() + .withHeader(null) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + } + + @Test + public void shouldOverwriteExistingIfHeadersMapContainsTheSameKey() throws Exception { + Map header = new HashMap(); + header.put("test", 456); + + String signed = JWTCreator.init() + .withClaim("test", 123) + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("test", 456)); + } + + @Test + public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { + Map header = new HashMap(); + header.put("test", null); + header.put("test2", "isSet"); + + String signed = JWTCreator.init() + .withClaim("test", 123) + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("test", null)); + assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet")); + } + @Test public void shouldAddKeyId() throws Exception { String signed = JWTCreator.init() From 7e33f20e4c67815c8ae1d425fe27a6ef7d19fb50 Mon Sep 17 00:00:00 2001 From: Maxim Balan Date: Thu, 24 Jan 2019 16:20:13 +0000 Subject: [PATCH 031/293] fix method description --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index b656d5f8..10a90df6 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -66,7 +66,7 @@ public static class Builder { /** * Add specific Claims to set as the Header. - * If provided map is null then nothing + * If provided map is null then nothing is changed * If provided map contains a header with null value then that header will be removed from the header claims * * @param headerClaims the values to use as Claims in the token's Header. From 8ca949eebe0d186567be72343c6e548ade001bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Tue, 29 Jan 2019 17:52:54 +0100 Subject: [PATCH 032/293] Performance improvements (#255) Performance improvements * Handle PublicClaims of date types in switch default case * Fix crypto test helper - allow minus (-) in token regexp * Add @Deprecated annotation and unit tests for deprecated signature methods * Formatting, remove unused imports, prefer StandardCharsets over apache commons Charsets --- lib/src/main/java/com/auth0/jwt/JWT.java | 26 ++- .../main/java/com/auth0/jwt/JWTCreator.java | 5 +- .../main/java/com/auth0/jwt/JWTDecoder.java | 5 +- .../main/java/com/auth0/jwt/JWTVerifier.java | 5 +- .../com/auth0/jwt/algorithms/Algorithm.java | 24 +++ .../auth0/jwt/algorithms/CryptoHelper.java | 175 ++++++++++++++++ .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 19 +- .../auth0/jwt/algorithms/HMACAlgorithm.java | 14 +- .../auth0/jwt/algorithms/NoneAlgorithm.java | 6 + .../auth0/jwt/algorithms/RSAAlgorithm.java | 17 +- .../java/com/auth0/jwt/impl/BasicHeader.java | 9 +- .../auth0/jwt/impl/HeaderDeserializer.java | 13 +- .../java/com/auth0/jwt/impl/JWTParser.java | 46 +++-- .../com/auth0/jwt/impl/JsonNodeClaim.java | 26 ++- .../auth0/jwt/impl/PayloadDeserializer.java | 17 +- .../java/com/auth0/jwt/impl/PayloadImpl.java | 13 +- .../com/auth0/jwt/impl/PayloadSerializer.java | 31 +-- .../java/com/auth0/jwt/interfaces/Claim.java | 7 +- lib/src/test/java/com/auth0/jwt/JWTTest.java | 18 ++ .../auth0/jwt/algorithms/AlgorithmTest.java | 27 +++ .../jwt/algorithms/CryptoTestHelper.java | 36 ++++ .../jwt/algorithms/ECDSAAlgorithmTest.java | 195 ++++++++++-------- .../ECDSABouncyCastleProviderTests.java | 133 +++++------- .../jwt/algorithms/HMACAlgorithmTest.java | 92 ++++----- .../jwt/algorithms/RSAAlgorithmTest.java | 162 ++++++++------- .../com/auth0/jwt/impl/BasicHeaderTest.java | 30 +-- .../jwt/impl/HeaderDeserializerTest.java | 7 +- .../com/auth0/jwt/impl/JWTParserTest.java | 39 ++-- .../com/auth0/jwt/impl/JsonNodeClaimTest.java | 15 +- .../jwt/impl/PayloadDeserializerTest.java | 2 +- .../com/auth0/jwt/impl/PayloadImplTest.java | 31 ++- 31 files changed, 823 insertions(+), 422 deletions(-) create mode 100644 lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java diff --git a/lib/src/main/java/com/auth0/jwt/JWT.java b/lib/src/main/java/com/auth0/jwt/JWT.java index f8e5cfc3..ca05deff 100644 --- a/lib/src/main/java/com/auth0/jwt/JWT.java +++ b/lib/src/main/java/com/auth0/jwt/JWT.java @@ -2,11 +2,35 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.impl.JWTParser; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; @SuppressWarnings("WeakerAccess") -public abstract class JWT { +public class JWT { + + private final JWTParser parser; + + /** + * Constructs a new instance of the JWT library. Use this if you need to decode many JWT + * tokens on the fly and do not wish to instantiate a new parser for each invocation. + */ + public JWT() { + parser = new JWTParser(); + } + + /** + * Decode a given Json Web Token. + *

+ * Note that this method doesn't verify the token's signature! Use it only if you trust the token or you already verified it. + * + * @param token with jwt format as string. + * @return a decoded JWT. + * @throws JWTDecodeException if any part of the token contained an invalid jwt or JSON format of each of the jwt parts. + */ + public DecodedJWT decodeJwt(String token) throws JWTDecodeException { + return new JWTDecoder(parser, token); + } /** * Decode a given Json Web Token. diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index a0889876..75d255ef 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -329,11 +329,10 @@ private void addClaim(String name, Object value) { private String sign() throws SignatureGenerationException { String header = Base64.encodeBase64URLSafeString(headerJson.getBytes(StandardCharsets.UTF_8)); String payload = Base64.encodeBase64URLSafeString(payloadJson.getBytes(StandardCharsets.UTF_8)); - String content = String.format("%s.%s", header, payload); - byte[] signatureBytes = algorithm.sign(content.getBytes(StandardCharsets.UTF_8)); + byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); String signature = Base64.encodeBase64URLSafeString((signatureBytes)); - return String.format("%s.%s", content, signature); + return String.format("%s.%s.%s", header, payload, signature); } } diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index 7921c128..34503f43 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -24,8 +24,11 @@ final class JWTDecoder implements DecodedJWT { private final Payload payload; JWTDecoder(String jwt) throws JWTDecodeException { + this(new JWTParser(), jwt); + } + + JWTDecoder(JWTParser converter, String jwt) throws JWTDecodeException { parts = TokenUtils.splitToken(jwt); - final JWTParser converter = new JWTParser(); String headerJson; String payloadJson; try { diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 26de4034..6eea86bb 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -2,6 +2,7 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; +import com.auth0.jwt.impl.JWTParser; import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Clock; @@ -18,11 +19,13 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { private final Algorithm algorithm; final Map claims; private final Clock clock; + private final JWTParser parser; JWTVerifier(Algorithm algorithm, Map claims, Clock clock) { this.algorithm = algorithm; this.claims = Collections.unmodifiableMap(claims); this.clock = clock; + this.parser = new JWTParser(); } /** @@ -363,7 +366,7 @@ private void requireClaim(String name, Object value) { */ @Override public DecodedJWT verify(String token) throws JWTVerificationException { - DecodedJWT jwt = JWT.decode(token); + DecodedJWT jwt = new JWTDecoder(parser, token); return verify(jwt); } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 3cc66d07..24ad025b 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -6,6 +6,7 @@ import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; +import java.io.ByteArrayOutputStream; import java.security.interfaces.*; /** @@ -361,12 +362,35 @@ public String toString() { */ public abstract void verify(DecodedJWT jwt) throws SignatureVerificationException; + /** + * Sign the given content using this Algorithm instance. + * + * @param headerBytes an array of bytes representing the base64 encoded header content to be verified against the signature. + * @param payloadBytes an array of bytes representing the base64 encoded payload content to be verified against the signature. + * @return the signature in a base64 encoded array of bytes + * @throws SignatureGenerationException if the Key is invalid. + */ + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + // default implementation; keep around until sign(byte[]) method is removed + byte[] contentBytes = new byte[headerBytes.length + 1 + payloadBytes.length]; + + System.arraycopy(headerBytes, 0, contentBytes, 0, headerBytes.length); + contentBytes[headerBytes.length] = (byte)'.'; + System.arraycopy(payloadBytes, 0, contentBytes, headerBytes.length + 1, payloadBytes.length); + + return sign(contentBytes); + } + /** * Sign the given content using this Algorithm instance. * * @param contentBytes an array of bytes representing the base64 encoded content to be verified against the signature. * @return the signature in a base64 encoded array of bytes * @throws SignatureGenerationException if the Key is invalid. + * @deprecated Please use the {@linkplain #sign(byte[], byte[])} method instead. */ + + @Deprecated public abstract byte[] sign(byte[] contentBytes) throws SignatureGenerationException; + } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index 08af662c..93e8f176 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -2,20 +2,181 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; + +import java.nio.charset.StandardCharsets; import java.security.*; class CryptoHelper { + private static final byte JWT_PART_SEPARATOR = (byte)46; + + /** + * Verify signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { + return verifySignatureFor(algorithm, secretBytes, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + } + + /** + * Verify signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { + return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), signatureBytes); + } + + /** + * Create signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException { + final Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(secretBytes, algorithm)); + mac.update(headerBytes); + mac.update(JWT_PART_SEPARATOR); + return mac.doFinal(payloadBytes); + } + + /** + * Verify signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param publicKey algorithm public key. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + boolean verifySignatureFor(String algorithm, PublicKey publicKey, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return verifySignatureFor(algorithm, publicKey, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + } + + /** + * Verify signature for JWT header and payload using a public key. + * + * @param algorithm algorithm name. + * @param publicKey the public key to use for verification. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Signature s = Signature.getInstance(algorithm); + s.initVerify(publicKey); + s.update(headerBytes); + s.update(JWT_PART_SEPARATOR); + s.update(payloadBytes); + return s.verify(signatureBytes); + } + + /** + * Create signature for JWT header and payload using a private key. + * + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. + */ + + byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Signature s = Signature.getInstance(algorithm); + s.initSign(privateKey); + s.update(headerBytes); + s.update(JWT_PART_SEPARATOR); + s.update(payloadBytes); + return s.sign(); + } + + /** + * Verify signature. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param contentBytes the content to which the signature applies. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @deprecated rather use corresponding method which takes header and payload as separate inputs + */ + + @Deprecated boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, contentBytes), signatureBytes); } + /** + * Create signature. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param contentBytes the content to be signed. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @deprecated rather use corresponding method which takes header and payload as separate inputs + */ + + @Deprecated byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes) throws NoSuchAlgorithmException, InvalidKeyException { final Mac mac = Mac.getInstance(algorithm); mac.init(new SecretKeySpec(secretBytes, algorithm)); return mac.doFinal(contentBytes); } + /** + * Verify signature using a public key. + * + * @param algorithm algorithm name. + * @param publicKey algorithm public key. + * @param contentBytes the content to which the signature applies. + * @param signatureBytes JWT signature. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. + * @deprecated rather use corresponding method which takes header and payload as separate inputs + */ + + @Deprecated boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initVerify(publicKey); @@ -23,6 +184,20 @@ boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] content return s.verify(signatureBytes); } + /** + * Create signature using a private key. + * + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param contentBytes the content to be signed. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. + * @deprecated rather use corresponding method which takes header and payload as separate inputs + */ + + @Deprecated byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] contentBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initSign(privateKey); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 08ff2885..12ddca70 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -6,7 +6,6 @@ import com.auth0.jwt.interfaces.ECDSAKeyProvider; import org.apache.commons.codec.binary.Base64; -import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; @@ -36,7 +35,6 @@ class ECDSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); try { @@ -44,7 +42,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); } - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, contentBytes, JOSEToDER(signatureBytes)); + boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); if (!valid) { throw new SignatureVerificationException(this); @@ -55,6 +53,21 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { } @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + ECPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); + } + byte[] signature = crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes); + return DERToJOSE(signature); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + throw new SignatureGenerationException(this, e); + } + } + + @Override + @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { ECPrivateKey privateKey = keyProvider.getPrivateKey(); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 670928f8..5df23a83 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -42,11 +42,10 @@ static byte[] getSecretBytes(String secret) throws IllegalArgumentException { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); try { - boolean valid = crypto.verifySignatureFor(getDescription(), secret, contentBytes, signatureBytes); + boolean valid = crypto.verifySignatureFor(getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } @@ -56,6 +55,16 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { } @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + return crypto.createSignatureFor(getDescription(), secret, headerBytes, payloadBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new SignatureGenerationException(this, e); + } + } + + @Override + @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { return crypto.createSignatureFor(getDescription(), secret, contentBytes); @@ -63,5 +72,4 @@ public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { throw new SignatureGenerationException(this, e); } } - } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java index 136cf886..f70b72b2 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java @@ -20,6 +20,12 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { } @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + return new byte[0]; + } + + @Override + @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { return new byte[0]; } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index b423e159..15cce55a 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -34,7 +34,6 @@ class RSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); try { @@ -42,7 +41,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); } - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, contentBytes, signatureBytes); + boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } @@ -51,6 +50,20 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { } } + @Override + @Deprecated + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + RSAPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); + } + return crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + throw new SignatureGenerationException(this, e); + } + } + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { diff --git a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java index 277724fd..b1d81a11 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java +++ b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java @@ -3,6 +3,7 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Header; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectReader; import java.util.Collections; import java.util.HashMap; @@ -19,13 +20,15 @@ class BasicHeader implements Header { private final String contentType; private final String keyId; private final Map tree; - - BasicHeader(String algorithm, String type, String contentType, String keyId, Map tree) { + private final ObjectReader objectReader; + + BasicHeader(String algorithm, String type, String contentType, String keyId, Map tree, ObjectReader objectReader) { this.algorithm = algorithm; this.type = type; this.contentType = contentType; this.keyId = keyId; this.tree = Collections.unmodifiableMap(tree == null ? new HashMap() : tree); + this.objectReader = objectReader; } Map getTree() { @@ -54,6 +57,6 @@ public String getKeyId() { @Override public Claim getHeaderClaim(String name) { - return extractClaim(name, tree); + return extractClaim(name, tree, objectReader); } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java index d42db420..cea2944a 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; @@ -12,12 +13,16 @@ class HeaderDeserializer extends StdDeserializer { - HeaderDeserializer() { - this(null); + private final ObjectReader objectReader; + + HeaderDeserializer(ObjectReader objectReader) { + this(null, objectReader); } - private HeaderDeserializer(Class vc) { + private HeaderDeserializer(Class vc, ObjectReader objectReader) { super(vc); + + this.objectReader = objectReader; } @Override @@ -32,7 +37,7 @@ public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws String type = getString(tree, PublicClaims.TYPE); String contentType = getString(tree, PublicClaims.CONTENT_TYPE); String keyId = getString(tree, PublicClaims.KEY_ID); - return new BasicHeader(algorithm, type, contentType, keyId, tree); + return new BasicHeader(algorithm, type, contentType, keyId, tree, objectReader); } String getString(Map tree, String claimName) { diff --git a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java index 45854785..84a8b867 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java @@ -6,13 +6,15 @@ import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import java.io.IOException; public class JWTParser implements JWTPartsParser { - private ObjectMapper mapper; + private final ObjectReader payloadReader; + private final ObjectReader headerReader; public JWTParser() { this(getDefaultObjectMapper()); @@ -20,23 +22,41 @@ public JWTParser() { JWTParser(ObjectMapper mapper) { addDeserializers(mapper); - this.mapper = mapper; + this.payloadReader = mapper.readerFor(Payload.class); + this.headerReader = mapper.readerFor(Header.class); } @Override public Payload parsePayload(String json) throws JWTDecodeException { - return convertFromJSON(json, Payload.class); + if (json == null) { + throw decodeException(); + } + + try { + return payloadReader.readValue(json); + } catch (IOException e) { + throw decodeException(json); + } } @Override public Header parseHeader(String json) throws JWTDecodeException { - return convertFromJSON(json, Header.class); + if (json == null) { + throw decodeException(); + } + + try { + return headerReader.readValue(json); + } catch (IOException e) { + throw decodeException(json); + } } private void addDeserializers(ObjectMapper mapper) { SimpleModule module = new SimpleModule(); - module.addDeserializer(Payload.class, new PayloadDeserializer()); - module.addDeserializer(Header.class, new HeaderDeserializer()); + ObjectReader reader = mapper.reader(); + module.addDeserializer(Payload.class, new PayloadDeserializer(reader)); + module.addDeserializer(Header.class, new HeaderDeserializer(reader)); mapper.registerModule(module); } @@ -47,19 +67,11 @@ static ObjectMapper getDefaultObjectMapper() { return mapper; } - @SuppressWarnings("WeakerAccess") - T convertFromJSON(String json, Class tClazz) throws JWTDecodeException { - if (json == null) { - throw exceptionForInvalidJson(null); - } - try { - return mapper.readValue(json, tClazz); - } catch (IOException e) { - throw exceptionForInvalidJson(json); - } + private static JWTDecodeException decodeException() { + return decodeException(null); } - private JWTDecodeException exceptionForInvalidJson(String json) { + private static JWTDecodeException decodeException(String json) { return new JWTDecodeException(String.format("The string '%s' doesn't have a valid JSON format.", json)); } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 98e76a21..4e2aef63 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import java.io.IOException; import java.lang.reflect.Array; @@ -20,10 +21,12 @@ */ class JsonNodeClaim implements Claim { + private final ObjectReader objectReader; private final JsonNode data; - private JsonNodeClaim(JsonNode node) { + private JsonNodeClaim(JsonNode node, ObjectReader objectReader) { this.data = node; + this.objectReader = objectReader; } @Override @@ -70,7 +73,7 @@ public T[] asArray(Class tClazz) throws JWTDecodeException { T[] arr = (T[]) Array.newInstance(tClazz, data.size()); for (int i = 0; i < data.size(); i++) { try { - arr[i] = getObjectMapper().treeToValue(data.get(i), tClazz); + arr[i] = objectReader.treeToValue(data.get(i), tClazz); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to " + tClazz.getSimpleName(), e); } @@ -87,7 +90,7 @@ public List asList(Class tClazz) throws JWTDecodeException { List list = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { try { - list.add(getObjectMapper().treeToValue(data.get(i), tClazz)); + list.add(objectReader.treeToValue(data.get(i), tClazz)); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to " + tClazz.getSimpleName(), e); } @@ -104,8 +107,7 @@ public Map asMap() throws JWTDecodeException { try { TypeReference> mapType = new TypeReference>() { }; - ObjectMapper thisMapper = getObjectMapper(); - JsonParser thisParser = thisMapper.treeAsTokens(data); + JsonParser thisParser = objectReader.treeAsTokens(data); return thisParser.readValueAs(mapType); } catch (IOException e) { throw new JWTDecodeException("Couldn't map the Claim value to Map", e); @@ -115,7 +117,7 @@ public Map asMap() throws JWTDecodeException { @Override public T as(Class tClazz) throws JWTDecodeException { try { - return getObjectMapper().treeAsTokens(data).readValueAs(tClazz); + return objectReader.treeAsTokens(data).readValueAs(tClazz); } catch (IOException e) { throw new JWTDecodeException("Couldn't map the Claim value to " + tClazz.getSimpleName(), e); } @@ -133,9 +135,9 @@ public boolean isNull() { * @param tree the JsonNode tree to search the Claim in. * @return a valid non-null Claim. */ - static Claim extractClaim(String claimName, Map tree) { + static Claim extractClaim(String claimName, Map tree, ObjectReader objectReader) { JsonNode node = tree.get(claimName); - return claimFromNode(node); + return claimFromNode(node, objectReader); } /** @@ -144,15 +146,11 @@ static Claim extractClaim(String claimName, Map tree) { * @param node the JsonNode to convert into a Claim. * @return a valid Claim instance. If the node is null or missing, a NullClaim will be returned. */ - static Claim claimFromNode(JsonNode node) { + static Claim claimFromNode(JsonNode node, ObjectReader objectReader) { if (node == null || node.isNull() || node.isMissingNode()) { return new NullClaim(); } - return new JsonNodeClaim(node); + return new JsonNodeClaim(node, objectReader); } - //Visible for testing - ObjectMapper getObjectMapper() { - return new ObjectMapper(); - } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 43047bfc..0c9de6c7 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; @@ -15,12 +15,16 @@ class PayloadDeserializer extends StdDeserializer { - PayloadDeserializer() { - this(null); + private final ObjectReader objectReader; + + PayloadDeserializer(ObjectReader reader) { + this(null, reader); } - private PayloadDeserializer(Class vc) { + private PayloadDeserializer(Class vc, ObjectReader reader) { super(vc); + + this.objectReader = reader; } @Override @@ -39,7 +43,7 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE Date issuedAt = getDateFromSeconds(tree, PublicClaims.ISSUED_AT); String jwtId = getString(tree, PublicClaims.JWT_ID); - return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree); + return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader); } List getStringOrArray(Map tree, String claimName) throws JWTDecodeException { @@ -51,11 +55,10 @@ List getStringOrArray(Map tree, String claimName) thro return Collections.singletonList(node.asText()); } - ObjectMapper mapper = new ObjectMapper(); List list = new ArrayList<>(node.size()); for (int i = 0; i < node.size(); i++) { try { - list.add(mapper.treeToValue(node.get(i), String.class)); + list.add(objectReader.treeToValue(node.get(i), String.class)); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to String", e); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index dced3062..d3b1dc0c 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -3,6 +3,7 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectReader; import java.util.*; @@ -20,8 +21,9 @@ class PayloadImpl implements Payload { private final Date issuedAt; private final String jwtId; private final Map tree; + private final ObjectReader objectReader; - PayloadImpl(String issuer, String subject, List audience, Date expiresAt, Date notBefore, Date issuedAt, String jwtId, Map tree) { + PayloadImpl(String issuer, String subject, List audience, Date expiresAt, Date notBefore, Date issuedAt, String jwtId, Map tree, ObjectReader objectReader) { this.issuer = issuer; this.subject = subject; this.audience = audience; @@ -29,7 +31,8 @@ class PayloadImpl implements Payload { this.notBefore = notBefore; this.issuedAt = issuedAt; this.jwtId = jwtId; - this.tree = Collections.unmodifiableMap(tree == null ? new HashMap() : tree); + this.tree = tree != null ? Collections.unmodifiableMap(tree) : Collections.emptyMap(); + this.objectReader = objectReader; } Map getTree() { @@ -73,14 +76,14 @@ public String getId() { @Override public Claim getClaim(String name) { - return extractClaim(name, tree); + return extractClaim(name, tree, objectReader); } @Override public Map getClaims() { - Map claims = new HashMap<>(); + Map claims = new HashMap<>(tree.size() * 2); for (String name : tree.keySet()) { - claims.put(name, extractClaim(name, tree)); + claims.put(name, extractClaim(name, tree, objectReader)); } return Collections.unmodifiableMap(claims); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index 29f287eb..dd0d2f42 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.util.Date; -import java.util.HashMap; import java.util.Map; public class PayloadSerializer extends StdSerializer { @@ -21,37 +20,41 @@ private PayloadSerializer(Class t) { @Override public void serialize(ClaimsHolder holder, JsonGenerator gen, SerializerProvider provider) throws IOException { - HashMap safePayload = new HashMap<>(); + + gen.writeStartObject(); for (Map.Entry e : holder.getClaims().entrySet()) { switch (e.getKey()) { case PublicClaims.AUDIENCE: if (e.getValue() instanceof String) { - safePayload.put(e.getKey(), e.getValue()); + gen.writeFieldName(e.getKey()); + gen.writeString((String)e.getValue()); break; } String[] audArray = (String[]) e.getValue(); if (audArray.length == 1) { - safePayload.put(e.getKey(), audArray[0]); + gen.writeFieldName(e.getKey()); + gen.writeString(audArray[0]); } else if (audArray.length > 1) { - safePayload.put(e.getKey(), audArray); + gen.writeFieldName(e.getKey()); + gen.writeStartArray(); + for(String aud : audArray) { + gen.writeString(aud); + } + gen.writeEndArray(); } break; - case PublicClaims.EXPIRES_AT: - case PublicClaims.ISSUED_AT: - case PublicClaims.NOT_BEFORE: - safePayload.put(e.getKey(), dateToSeconds((Date) e.getValue())); - break; default: - if (e.getValue() instanceof Date) { - safePayload.put(e.getKey(), dateToSeconds((Date) e.getValue())); + gen.writeFieldName(e.getKey()); + if (e.getValue() instanceof Date) { // true for EXPIRES_AT, ISSUED_AT, NOT_BEFORE + gen.writeNumber(dateToSeconds((Date) e.getValue())); } else { - safePayload.put(e.getKey(), e.getValue()); + gen.writeObject(e.getValue()); } break; } } - gen.writeObject(safePayload); + gen.writeEndObject(); } private long dateToSeconds(Date date) { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index 893becd2..b3d41b1d 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -69,7 +69,8 @@ public interface Claim { /** * Get this Claim as an Array of type T. * If the value isn't an Array, null will be returned. - * + * @param type + * @param tClazz the type class * @return the value as an Array or null. * @throws JWTDecodeException if the values inside the Array can't be converted to a class T. */ @@ -79,6 +80,8 @@ public interface Claim { * Get this Claim as a List of type T. * If the value isn't an Array, null will be returned. * + * @param type + * @param tClazz the type class * @return the value as a List or null. * @throws JWTDecodeException if the values inside the List can't be converted to a class T. */ @@ -95,6 +98,8 @@ public interface Claim { /** * Get this Claim as a custom type T. * + * @param type + * @param tClazz the type class * @return the value as instance of T. * @throws JWTDecodeException if the value can't be converted to a class T. */ diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 579ea271..7cbe4b22 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -46,6 +46,15 @@ public void shouldDecodeAStringToken() throws Exception { assertThat(jwt, is(notNullValue())); } + @Test + public void shouldDecodeAStringTokenUsingInstance() throws Exception { + String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + JWT jwt = new JWT(); + DecodedJWT decodedJWT = jwt.decodeJwt(token); + + assertThat(decodedJWT, is(notNullValue())); + } + // getToken @Test public void shouldGetStringToken() throws Exception { @@ -55,6 +64,15 @@ public void shouldGetStringToken() throws Exception { assertThat(jwt.getToken(), is("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); } + // getToken + @Test + public void shouldGetStringTokenUsingInstance() throws Exception { + JWT jwt = new JWT(); + DecodedJWT decodedJWT = jwt.decodeJwt("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + assertThat(decodedJWT, is(notNullValue())); + assertThat(decodedJWT.getToken(), is(notNullValue())); + assertThat(decodedJWT.getToken(), is("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); + } // Verify diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java index 6dcbbae9..f4f437f9 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -5,7 +5,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.security.interfaces.*; @@ -13,6 +15,8 @@ import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.withSettings; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; public class AlgorithmTest { @@ -546,4 +550,27 @@ public void shouldCreateNoneAlgorithm() throws Exception { assertThat(algorithm.getName(), is("none")); } + @Test + public void shouldForwardHeaderPayloadSignatureToSiblingSignMethodForBackwardsCompatibility() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + + ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(byte[].class); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + byte[] signature = new byte[]{0x10, 0x11, 0x12}; + when(algorithm.sign(any(byte[].class), any(byte[].class))).thenCallRealMethod(); + when(algorithm.sign(contentCaptor.capture())).thenReturn(signature); + + byte[] sign = algorithm.sign(header, payload); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(sign, is(signature)); + assertThat(contentCaptor.getValue(), is(bout.toByteArray())); + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java new file mode 100644 index 00000000..f977e42a --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java @@ -0,0 +1,36 @@ +package com.auth0.jwt.algorithms; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.binary.Base64; + +public abstract class CryptoTestHelper { + + private static final Pattern authHeaderPattern = Pattern.compile("^([\\w-]+)\\.([\\w-]+)\\.([\\w-]+)"); + + public static String asJWT(Algorithm algorithm, String header, String payload) { + byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); + String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); + return String.format("%s.%s.%s", header, payload, jwtSignature); + } + + public static void assertSignatureValue(String jwt, String expectedSignature) { + String jwtSignature = jwt.substring(jwt.lastIndexOf('.') + 1); + assertThat(jwtSignature, is(expectedSignature)); + } + + public static void assertSignaturePresent(String jwt) { + Matcher matcher = authHeaderPattern.matcher(jwt); + if (!matcher.find() || matcher.groupCount() < 3) { + fail("No signature present in " + jwt); + } + + assertThat(matcher.group(3), not(is(emptyString()))); + } +} diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 86bf3c0b..8968ed9f 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -4,6 +4,7 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.ECDSAKeyProvider; + import org.apache.commons.codec.binary.Base64; import org.hamcrest.Matchers; import org.hamcrest.collection.IsIn; @@ -12,6 +13,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.ECKey; @@ -19,8 +21,11 @@ import java.security.interfaces.ECPublicKey; import java.util.Arrays; + import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; +import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; +import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; @@ -456,7 +461,7 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -474,7 +479,7 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -492,7 +497,7 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -509,30 +514,30 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception private static final String ES512Header = "eyJhbGciOiJFUzUxMiJ9"; private static final String auth0IssPayload = "eyJpc3MiOiJhdXRoMCJ9"; + private static final byte[] ES256HeaderBytes = ES256Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] ES384HeaderBytes = ES384Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] ES512HeaderBytes = ES512Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] auth0IssPayloadBytes = auth0IssPayload.getBytes(StandardCharsets.UTF_8); + + @Test public void shouldDoECDSA256Signing() throws Exception { - Algorithm algorithmSign = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); + byte[] signatureBytes = algorithm.sign(ES256HeaderBytes, auth0IssPayloadBytes); String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = String.format("%s.%s.%s", ES256Header, auth0IssPayload, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -544,13 +549,10 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA256(provider); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -564,7 +566,7 @@ public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -575,33 +577,25 @@ public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -613,13 +607,10 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA384(provider); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -633,7 +624,7 @@ public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -644,33 +635,27 @@ public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithmSign, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -683,13 +668,10 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA512(provider); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -703,7 +685,7 @@ public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -714,7 +696,7 @@ public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -724,14 +706,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test @@ -741,14 +723,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test @@ -758,14 +740,14 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test @@ -795,7 +777,7 @@ public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequence ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; - byte[] signature = algorithm256.sign(content256.getBytes()); + byte[] signature = algorithm256.sign(content256.getBytes(), new byte[0]); signature[0] = (byte) 0x02; algorithm256.DERToJOSE(signature); } @@ -848,13 +830,11 @@ public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() th @Test public void shouldSignAndVerifyWithECDSA256() throws Exception { ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm256.sign(content256.getBytes()); - String signature256 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content256 + "." + signature256; + String jwt = asJWT(algorithm256, header256, body); algorithm256.verify(JWT.decode(jwt)); } } @@ -862,13 +842,11 @@ public void shouldSignAndVerifyWithECDSA256() throws Exception { @Test public void shouldSignAndVerifyWithECDSA384() throws Exception { ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String content384 = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm384.sign(content384.getBytes()); - String signature384 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content384 + "." + signature384; + String jwt = asJWT(algorithm384, header384, body); algorithm384.verify(JWT.decode(jwt)); } } @@ -876,13 +854,11 @@ public void shouldSignAndVerifyWithECDSA384() throws Exception { @Test public void shouldSignAndVerifyWithECDSA512() throws Exception { ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String content512 = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm512.sign(content512.getBytes()); - String signature512 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content512 + "." + signature512; + String jwt = asJWT(algorithm512, header512, body); algorithm512.verify(JWT.decode(jwt)); } } @@ -1164,4 +1140,53 @@ static void assertValidDERSignature(byte[] derSignature, int numberSize, boolean Assert.assertThat(derSignature.length, is(totalLength)); } + @Test + public void shouldBeEqualSignatureMethodDecodeResults() throws Exception { + // signatures are not deterministic in value, so instead of directly comparing the signatures, + // check that both sign(..) methods can be used to create a jwt which can be + // verified + Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + String header = "eyJhbGciOiJFUzI1NiJ9"; + String payload = "eyJpc3MiOiJhdXRoMCJ9"; + + byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); + byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(headerBytes); + bout.write('.'); + bout.write(payloadBytes); + + String jwtSignature1 = Base64.encodeBase64URLSafeString(algorithm.sign(bout.toByteArray())); + String jwt1 = String.format("%s.%s.%s", header, payload, jwtSignature1); + + algorithm.verify(JWT.decode(jwt1)); + + String jwtSignature2 = Base64.encodeBase64URLSafeString(algorithm.sign(headerBytes, payloadBytes)); + String jwt2 = String.format("%s.%s.%s", header, payload, jwtSignature2); + + algorithm.verify(JWT.decode(jwt2)); + } + + /** + * Test deprecated signing method error handling. + * + * @see {@linkplain #shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull} + * @throws Exception expected exception + */ + + @Test + public void shouldFailOnECDSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.sign(new byte[0]); + } + } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java index 4af1c4ec..452c0074 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -1,4 +1,5 @@ package com.auth0.jwt.algorithms; +import static com.auth0.jwt.algorithms.CryptoTestHelper.*; import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.SignatureGenerationException; @@ -473,7 +474,7 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -491,7 +492,7 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -509,7 +510,7 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); @@ -530,26 +531,18 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception public void shouldDoECDSA256Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -561,13 +554,10 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA256(provider); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -581,7 +571,7 @@ public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -592,33 +582,25 @@ public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -630,13 +612,10 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA384(provider); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -650,7 +629,7 @@ public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -661,33 +640,26 @@ public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithmSign, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -700,13 +672,9 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA512(provider); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -720,7 +688,7 @@ public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -731,7 +699,7 @@ public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -741,14 +709,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test @@ -758,14 +726,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test @@ -775,14 +743,14 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test @@ -812,7 +780,7 @@ public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequence ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; - byte[] signature = algorithm256.sign(content256.getBytes()); + byte[] signature = algorithm256.sign(content256.getBytes(), new byte[0]); signature[0] = (byte) 0x02; algorithm256.DERToJOSE(signature); } @@ -865,13 +833,11 @@ public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() th @Test public void shouldSignAndVerifyWithECDSA256() throws Exception { ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm256.sign(content256.getBytes()); - String signature256 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content256 + "." + signature256; + String jwt = asJWT(algorithm256, header256, body); algorithm256.verify(JWT.decode(jwt)); } } @@ -879,13 +845,11 @@ public void shouldSignAndVerifyWithECDSA256() throws Exception { @Test public void shouldSignAndVerifyWithECDSA384() throws Exception { ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String content384 = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm384.sign(content384.getBytes()); - String signature384 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content384 + "." + signature384; + String jwt = asJWT(algorithm384, header384, body); algorithm384.verify(JWT.decode(jwt)); } } @@ -893,13 +857,11 @@ public void shouldSignAndVerifyWithECDSA384() throws Exception { @Test public void shouldSignAndVerifyWithECDSA512() throws Exception { ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String content512 = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm512.sign(content512.getBytes()); - String signature512 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content512 + "." + signature512; + String jwt = asJWT(algorithm512, header512, body); algorithm512.verify(JWT.decode(jwt)); } } @@ -1054,4 +1016,5 @@ public void shouldDecodeECDSA512DER() throws Exception { assertValidJOSESignature(joseSignature, 66, true, true); } + } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 2d5fd301..b70c8633 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -4,16 +4,19 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; +import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; +import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignatureValue; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -127,7 +130,7 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); @@ -142,7 +145,7 @@ public void shouldThrowOnVerifyWhenTheSecretIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); @@ -161,15 +164,11 @@ public void shouldThrowOnVerifyWhenTheSecretIsInvalid() throws Exception { public void shouldDoHMAC256SigningWithBytes() throws Exception { Algorithm algorithm = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); - String jwtContent = String.format("%s.%s", HS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -177,15 +176,11 @@ public void shouldDoHMAC256SigningWithBytes() throws Exception { public void shouldDoHMAC384SigningWithBytes() throws Exception { Algorithm algorithm = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); - String jwtContent = String.format("%s.%s", HS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -193,15 +188,11 @@ public void shouldDoHMAC384SigningWithBytes() throws Exception { public void shouldDoHMAC512SigningWithBytes() throws Exception { Algorithm algorithm = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); - String jwtContent = String.format("%s.%s", HS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -209,15 +200,11 @@ public void shouldDoHMAC512SigningWithBytes() throws Exception { public void shouldDoHMAC256SigningWithString() throws Exception { Algorithm algorithm = Algorithm.HMAC256("secret"); - String jwtContent = String.format("%s.%s", HS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -225,15 +212,11 @@ public void shouldDoHMAC256SigningWithString() throws Exception { public void shouldDoHMAC384SigningWithString() throws Exception { Algorithm algorithm = Algorithm.HMAC384("secret"); - String jwtContent = String.format("%s.%s", HS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -241,15 +224,11 @@ public void shouldDoHMAC384SigningWithString() throws Exception { public void shouldDoHMAC512SigningWithString() throws Exception { Algorithm algorithm = Algorithm.HMAC512("secret"); - String jwtContent = String.format("%s.%s", HS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm ,HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -260,11 +239,11 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -274,11 +253,11 @@ public void shouldThrowOnSignWhenTheSecretIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -286,4 +265,19 @@ public void shouldReturnNullSigningKeyId() throws Exception { assertThat(Algorithm.HMAC256("secret").getSigningKeyId(), is(nullValue())); } + @Test + public void shouldBeEqualSignatureMethodResults() throws Exception { + Algorithm algorithm = Algorithm.HMAC256("secret"); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); + } + } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java index 9025f9e4..b24365e3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -4,12 +4,11 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.RSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.nio.charset.StandardCharsets; +import java.io.ByteArrayOutputStream; import java.security.*; import java.security.interfaces.RSAKey; import java.security.interfaces.RSAPrivateKey; @@ -24,6 +23,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static com.auth0.jwt.algorithms.CryptoTestHelper.*; @SuppressWarnings("deprecation") public class RSAAlgorithmTest { @@ -213,9 +213,9 @@ public void shouldThrowWhenMacAlgorithmDoesNotExists() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: some-alg"); exception.expectCause(isA(NoSuchAlgorithmException.class)); - + CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -233,7 +233,7 @@ public void shouldThrowWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -251,7 +251,7 @@ public void shouldThrowWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -274,15 +274,11 @@ public void shouldDoRSA256Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA256((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS256Header, auth0IssPayload); String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -290,15 +286,11 @@ public void shouldDoRSA256Signing() throws Exception { public void shouldDoRSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -310,13 +302,10 @@ public void shouldDoRSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA256(provider); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -330,7 +319,7 @@ public void shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -341,7 +330,7 @@ public void shouldFailOnRSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -349,15 +338,11 @@ public void shouldDoRSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA384((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS384Header, auth0IssPayload); String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -365,15 +350,11 @@ public void shouldDoRSA384Signing() throws Exception { public void shouldDoRSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA384((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -385,13 +366,10 @@ public void shouldDoRSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA384(provider); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -405,7 +383,7 @@ public void shouldFailOnRSA384SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -416,7 +394,7 @@ public void shouldFailOnRSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -424,15 +402,11 @@ public void shouldDoRSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA512((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS512Header, auth0IssPayload); String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -440,15 +414,11 @@ public void shouldDoRSA512Signing() throws Exception { public void shouldDoRSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA512((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -460,13 +430,10 @@ public void shouldDoRSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA512(provider); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -480,7 +447,7 @@ public void shouldFailOnRSA512SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -491,7 +458,7 @@ public void shouldFailOnRSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -501,14 +468,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -518,14 +485,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -535,14 +502,14 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -563,4 +530,43 @@ public void shouldReturnSigningKeyIdFromProvider() throws Exception { assertThat(algorithm.getSigningKeyId(), is("keyId")); } + + @Test + public void shouldBeEqualSignatureMethodResults() throws Exception { + RSAPrivateKey privateKey = (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA"); + RSAPublicKey publicKey = (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + + Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); + } + + /** + * Test deprecated signing method error handling. + * + * @see {@linkplain #shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull} + * @throws Exception expected exception + */ + + @Test + public void shouldFailOnRSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withRSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.RSA256(provider); + algorithm.sign(new byte[0]); + } + } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java index d96baacf..c2d89556 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java @@ -1,6 +1,8 @@ package com.auth0.jwt.impl; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.TextNode; import org.hamcrest.collection.IsMapContaining; @@ -18,19 +20,21 @@ public class BasicHeaderTest { @Rule public ExpectedException exception = ExpectedException.none(); + + private ObjectReader objectReader = new ObjectMapper().reader(); @SuppressWarnings("Convert2Diamond") @Test public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNonNullTree() throws Exception { exception.expect(UnsupportedOperationException.class); - BasicHeader header = new BasicHeader(null, null, null, null, new HashMap()); + BasicHeader header = new BasicHeader(null, null, null, null, new HashMap(), objectReader); header.getTree().put("something", null); } @Test public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNullTree() throws Exception { exception.expect(UnsupportedOperationException.class); - BasicHeader header = new BasicHeader(null, null, null, null, null); + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); header.getTree().put("something", null); } @@ -39,7 +43,7 @@ public void shouldHaveTree() throws Exception { HashMap map = new HashMap<>(); JsonNode node = NullNode.getInstance(); map.put("key", node); - BasicHeader header = new BasicHeader(null, null, null, null, map); + BasicHeader header = new BasicHeader(null, null, null, null, map, objectReader); assertThat(header.getTree(), is(notNullValue())); assertThat(header.getTree(), is(IsMapContaining.hasEntry("key", node))); @@ -47,7 +51,7 @@ public void shouldHaveTree() throws Exception { @Test public void shouldGetAlgorithm() throws Exception { - BasicHeader header = new BasicHeader("HS256", null, null, null, null); + BasicHeader header = new BasicHeader("HS256", null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getAlgorithm(), is(notNullValue())); @@ -56,7 +60,7 @@ public void shouldGetAlgorithm() throws Exception { @Test public void shouldGetNullAlgorithmIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getAlgorithm(), is(nullValue())); @@ -64,7 +68,7 @@ public void shouldGetNullAlgorithmIfMissing() throws Exception { @Test public void shouldGetType() throws Exception { - BasicHeader header = new BasicHeader(null, "jwt", null, null, null); + BasicHeader header = new BasicHeader(null, "jwt", null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getType(), is(notNullValue())); @@ -73,7 +77,7 @@ public void shouldGetType() throws Exception { @Test public void shouldGetNullTypeIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getType(), is(nullValue())); @@ -81,7 +85,7 @@ public void shouldGetNullTypeIfMissing() throws Exception { @Test public void shouldGetContentType() throws Exception { - BasicHeader header = new BasicHeader(null, null, "content", null, null); + BasicHeader header = new BasicHeader(null, null, "content", null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getContentType(), is(notNullValue())); @@ -90,7 +94,7 @@ public void shouldGetContentType() throws Exception { @Test public void shouldGetNullContentTypeIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getContentType(), is(nullValue())); @@ -98,7 +102,7 @@ public void shouldGetNullContentTypeIfMissing() throws Exception { @Test public void shouldGetKeyId() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, "key", null); + BasicHeader header = new BasicHeader(null, null, null, "key", null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getKeyId(), is(notNullValue())); @@ -107,7 +111,7 @@ public void shouldGetKeyId() throws Exception { @Test public void shouldGetNullKeyIdIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getKeyId(), is(nullValue())); @@ -117,7 +121,7 @@ public void shouldGetNullKeyIdIfMissing() throws Exception { public void shouldGetExtraClaim() throws Exception { Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); - BasicHeader header = new BasicHeader(null, null, null, null, tree); + BasicHeader header = new BasicHeader(null, null, null, null, tree, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getHeaderClaim("extraClaim"), is(instanceOf(JsonNodeClaim.class))); @@ -127,7 +131,7 @@ public void shouldGetExtraClaim() throws Exception { @Test public void shouldGetNotNullExtraClaimIfMissing() throws Exception { Map tree = new HashMap<>(); - BasicHeader header = new BasicHeader(null, null, null, null, tree); + BasicHeader header = new BasicHeader(null, null, null, null, tree, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getHeaderClaim("missing"), is(notNullValue())); diff --git a/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java index 3b6af385..dffcab50 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.TextNode; import org.junit.Before; @@ -33,11 +34,11 @@ public class HeaderDeserializerTest { @Rule public ExpectedException exception = ExpectedException.none(); private HeaderDeserializer deserializer; - + private ObjectReader objectReader = new ObjectMapper().reader(); @Before public void setUp() throws Exception { - deserializer = new HeaderDeserializer(); + deserializer = new HeaderDeserializer(objectReader); } @Test @@ -45,7 +46,7 @@ public void shouldThrowOnNullTree() throws Exception { exception.expect(JWTDecodeException.class); exception.expectMessage("Parsing the Header's JSON resulted on a Null map"); - JsonDeserializer deserializer = new HeaderDeserializer(); + JsonDeserializer deserializer = new HeaderDeserializer(objectReader); JsonParser parser = mock(JsonParser.class); ObjectCodec codec = mock(ObjectCodec.class); DeserializationContext context = mock(DeserializationContext.class); diff --git a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java index 1c990fcb..4f97b2bd 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java @@ -5,6 +5,7 @@ import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; import org.junit.Before; import org.junit.Rule; @@ -17,6 +18,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class JWTParserTest { @@ -47,10 +49,12 @@ public void shouldAddDeserializers() throws Exception { @Test public void shouldParsePayload() throws Exception { ObjectMapper mapper = mock(ObjectMapper.class); + ObjectReader reader = mock(ObjectReader.class); + when(mapper.readerFor(Payload.class)).thenReturn(reader); JWTParser parser = new JWTParser(mapper); parser.parsePayload("{}"); - verify(mapper).readValue("{}", Payload.class); + verify(reader).readValue("{}"); } @Test @@ -65,10 +69,12 @@ public void shouldThrowOnInvalidPayload() throws Exception { @Test public void shouldParseHeader() throws Exception { ObjectMapper mapper = mock(ObjectMapper.class); + ObjectReader reader = mock(ObjectReader.class); + when(mapper.readerFor(Header.class)).thenReturn(reader); JWTParser parser = new JWTParser(mapper); parser.parseHeader("{}"); - verify(mapper).readValue("{}", Header.class); + verify(reader).readValue("{}"); } @Test @@ -81,27 +87,30 @@ public void shouldThrowOnInvalidHeader() throws Exception { } @Test - public void shouldConvertFromValidJSON() throws Exception { - String json = "\r\n { \r\n } \r\n"; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(notNullValue())); + public void shouldThrowWhenConvertingHeaderIfNullJson() throws Exception { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The string 'null' doesn't have a valid JSON format."); + parser.parseHeader(null); + } + + @Test + public void shouldThrowWhenConvertingHeaderFromInvalidJson() throws Exception { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The string '}{' doesn't have a valid JSON format."); + parser.parseHeader("}{"); } @Test - public void shouldThrowWhenConvertingIfNullJson() throws Exception { + public void shouldThrowWhenConvertingPayloadIfNullJson() throws Exception { exception.expect(JWTDecodeException.class); exception.expectMessage("The string 'null' doesn't have a valid JSON format."); - String json = null; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(nullValue())); + parser.parsePayload(null); } @Test - public void shouldThrowWhenConvertingFromInvalidJson() throws Exception { + public void shouldThrowWhenConvertingPayloadFromInvalidJson() throws Exception { exception.expect(JWTDecodeException.class); exception.expectMessage("The string '}{' doesn't have a valid JSON format."); - String json = "}{"; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(nullValue())); + parser.parsePayload("}{"); } -} \ No newline at end of file +} diff --git a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java index 944860f9..1a82fcec 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.MissingNode; import com.fasterxml.jackson.databind.node.NullNode; @@ -32,12 +33,15 @@ public class JsonNodeClaimTest { private ObjectMapper mapper; + private ObjectReader objectReader; + @Rule public ExpectedException exception = ExpectedException.none(); @Before public void setUp() throws Exception { mapper = getDefaultObjectMapper(); + objectReader = mapper.reader(); } @Test @@ -49,6 +53,10 @@ public void shouldGetBooleanValue() throws Exception { assertThat(claim.asBoolean(), is(true)); } + private Claim claimFromNode(JsonNode value) { + return JsonNodeClaim.claimFromNode(value, objectReader); + } + @Test public void shouldGetNullBooleanIfNotBooleanValue() throws Exception { JsonNode objectValue = mapper.valueToTree(new Object()); @@ -270,10 +278,11 @@ public void shouldThrowIfAnExtraordinaryExceptionHappensWhenParsingAsGenericMap( JsonNode value = mock(ObjectNode.class); when(value.getNodeType()).thenReturn(JsonNodeType.OBJECT); - JsonNodeClaim claim = (JsonNodeClaim) claimFromNode(value); + ObjectReader mockedMapper = mock(ObjectReader.class); + + JsonNodeClaim claim = (JsonNodeClaim) JsonNodeClaim.claimFromNode(value, mockedMapper); JsonNodeClaim spiedClaim = spy(claim); - ObjectMapper mockedMapper = mock(ObjectMapper.class); - when(spiedClaim.getObjectMapper()).thenReturn(mockedMapper); + JsonParser mockedParser = mock(JsonParser.class); when(mockedMapper.treeAsTokens(value)).thenReturn(mockedParser); when(mockedParser.readValueAs(ArgumentMatchers.any(TypeReference.class))).thenThrow(IOException.class); diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java index 2e2cabed..9a82878e 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -36,7 +36,7 @@ public class PayloadDeserializerTest { @Before public void setUp() throws Exception { - deserializer = new PayloadDeserializer(); + deserializer = new PayloadDeserializer(new ObjectMapper().reader()); } @Test diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java index ca7562dc..56654427 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -2,6 +2,8 @@ import com.auth0.jwt.interfaces.Claim; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.TextNode; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.core.IsCollectionContaining; @@ -16,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -29,21 +32,27 @@ public class PayloadImplTest { private Date notBefore; private Date issuedAt; + private ObjectMapper mapper; + private ObjectReader objectReader; + @Before public void setUp() throws Exception { + mapper = getDefaultObjectMapper(); + objectReader = mapper.reader(); + expiresAt = Mockito.mock(Date.class); notBefore = Mockito.mock(Date.class); issuedAt = Mockito.mock(Date.class); Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); - payload = new PayloadImpl("issuer", "subject", Collections.singletonList("audience"), expiresAt, notBefore, issuedAt, "jwtId", tree); + payload = new PayloadImpl("issuer", "subject", Collections.singletonList("audience"), expiresAt, notBefore, issuedAt, "jwtId", tree, objectReader); } @SuppressWarnings("Convert2Diamond") @Test public void shouldHaveUnmodifiableTree() throws Exception { exception.expect(UnsupportedOperationException.class); - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, new HashMap()); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, new HashMap(), objectReader); payload.getTree().put("something", null); } @@ -55,7 +64,7 @@ public void shouldGetIssuer() throws Exception { @Test public void shouldGetNullIssuerIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getIssuer(), is(nullValue())); } @@ -68,7 +77,7 @@ public void shouldGetSubject() throws Exception { @Test public void shouldGetNullSubjectIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getSubject(), is(nullValue())); } @@ -83,7 +92,7 @@ public void shouldGetAudience() throws Exception { @Test public void shouldGetNullAudienceIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getAudience(), is(nullValue())); } @@ -96,7 +105,7 @@ public void shouldGetExpiresAt() throws Exception { @Test public void shouldGetNullExpiresAtIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getExpiresAt(), is(nullValue())); } @@ -109,7 +118,7 @@ public void shouldGetNotBefore() throws Exception { @Test public void shouldGetNullNotBeforeIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getNotBefore(), is(nullValue())); } @@ -122,7 +131,7 @@ public void shouldGetIssuedAt() throws Exception { @Test public void shouldGetNullIssuedAtIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getIssuedAt(), is(nullValue())); } @@ -135,7 +144,7 @@ public void shouldGetJWTId() throws Exception { @Test public void shouldGetNullJWTIdIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getId(), is(nullValue())); } @@ -149,7 +158,7 @@ public void shouldGetExtraClaim() throws Exception { @Test public void shouldGetNotNullExtraClaimIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getClaim("missing"), is(notNullValue())); assertThat(payload.getClaim("missing"), is(instanceOf(NullClaim.class))); @@ -160,7 +169,7 @@ public void shouldGetClaims() throws Exception { Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); tree.put("sub", new TextNode("auth0")); - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, tree); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, tree, objectReader); assertThat(payload, is(notNullValue())); Map claims = payload.getClaims(); assertThat(claims, is(notNullValue())); From a682f9d8fe469adac0d557b5c15a154178d4a6f4 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 29 Jan 2019 14:19:35 -0300 Subject: [PATCH 033/293] Release 3.7.0 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e0aef02..e951218b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.7.0](https://github.com/auth0/java-jwt/tree/3.7.0) (2019-01-29) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.6.0...3.7.0) + +**Added** +- Performance improvements [\#255](https://github.com/auth0/java-jwt/pull/255) ([skjolber](https://github.com/skjolber)) + ## [3.6.0](https://github.com/auth0/java-jwt/tree/3.6.0) (2019-01-24) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.5.0...3.6.0) diff --git a/README.md b/README.md index 5b8c7e07..f584a2d8 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.6.0 + 3.7.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.6.0' +implementation 'com.auth0:java-jwt:3.7.0' ``` ## Available Algorithms From 7fcd1bfece8197f0c44cd020a03045df6cf182de Mon Sep 17 00:00:00 2001 From: Josh Cunningham Date: Mon, 4 Feb 2019 16:02:43 -0800 Subject: [PATCH 034/293] Adding GitHub Issue and PR templates (#323) Adding GitHub Issue and PR templates --- .github/ISSUE_TEMPLATE.md | 38 ++++++++++++++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 31 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..009ccd24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,38 @@ +In order to efficiently and accurately address your issue or feature request, please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. Please delete any sections or questions below that do not pertain to this request. + +For general support or usage questions, please use the [Auth0 Community](https://community.auth0.com/) or [Auth0 Support](https://support.auth0.com/). + +### Description + +Description of the bug or feature request and why it's a problem. Consider including: + +- The use case or overall problem you're trying to solve +- Information about when the problem started + +### Prerequisites + +- [ ] I have checked the documentation for this library in the README. +- [ ] I have checked the [Auth0 Community](https://community.auth0.com/) for related posts. +- [ ] I have checked for related or duplicate [Issues](https://github.com/auth0/java-jwt/issues) and [PRs](https://github.com/auth0/java-jwt/pulls). +- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). +- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). +- [ ] I am reporting this to the correct repository (note this library is used by many libraries, such as [auth0-java](https://github.com/auth0/auth0-java)). + +### Environment + +Please provide the following: + +- Version of this library used: +- Version of Java framework used: +- Additional libraries that might be affecting your instance: + +### Reproduction + +Detail the steps taken to reproduce this error and note if this issue can be reproduced consistently or if it is intermittent. + +Please include: + +- Code sample to reproduce the issue +- Log files (redact/remove sensitive information) +- Application settings (redact/remove sensitive information) +- Screenshots, if helpful diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..8675fc7e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +### Changes + +Please describe both what is changing and why this is important. Include: + +- Endpoints added, deleted, deprecated, or changed +- Classes and methods added, deleted, deprecated, or changed +- Screenshots of new or changed UI, if applicable +- A summary of usage if this is a new feature or change to a public API (this should also be added to relevant documentation once released) +- Any alternative designs or approaches considered + +### References + +Please include relevant links supporting this change such as a: + +- support ticket +- community post +- StackOverflow post +- support forum thread + +### Testing + +Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. + +- [ ] This change adds test coverage +- [ ] This change has been tested on the latest version of Java or why not + +### Checklist + +- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) +- [ ] All existing and new tests complete without errors From 6ffa7032d26ebd69a16b9ed62bd8eef628aee133 Mon Sep 17 00:00:00 2001 From: itdevelopmentapps <31443120+itdevelopmentapps@users.noreply.github.com> Date: Mon, 11 Mar 2019 17:57:53 +0100 Subject: [PATCH 035/293] Support multiple issuers #246 (#288) * Support multiple issuers #246 * Implemented comments after review * Implemented comments after review, rolled back remove claim unit test * Implemented comments after review * Added tests to increase coverage --- .../main/java/com/auth0/jwt/JWTVerifier.java | 17 ++++++--- .../auth0/jwt/interfaces/Verification.java | 2 +- .../java/com/auth0/jwt/JWTVerifierTest.java | 38 ++++++++++++++++++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 6eea86bb..a1c7f82b 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -61,12 +61,12 @@ public static class BaseVerification implements Verification { /** * Require a specific Issuer ("iss") claim. * - * @param issuer the required Issuer value + * @param issuer the required Issuer value. If multiple values are given, the claim must at least match one of them * @return this same Verification instance. */ @Override - public Verification withIssuer(String issuer) { - requireClaim(PublicClaims.ISSUER, issuer); + public Verification withIssuer(String... issuer) { + requireClaim(PublicClaims.ISSUER, issuer == null ? null : Arrays.asList(issuer)); return this; } @@ -90,7 +90,7 @@ public Verification withSubject(String subject) { */ @Override public Verification withAudience(String... audience) { - requireClaim(PublicClaims.AUDIENCE, Arrays.asList(audience)); + requireClaim(PublicClaims.AUDIENCE, audience == null ? null : Arrays.asList(audience)); return this; } @@ -398,7 +398,6 @@ private void verifyClaims(DecodedJWT jwt, Map claims) throws Tok for (Map.Entry entry : claims.entrySet()) { switch (entry.getKey()) { case PublicClaims.AUDIENCE: - //noinspection unchecked assertValidAudienceClaim(jwt.getAudience(), (List) entry.getValue()); break; case PublicClaims.EXPIRES_AT: @@ -411,7 +410,7 @@ private void verifyClaims(DecodedJWT jwt, Map claims) throws Tok assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false); break; case PublicClaims.ISSUER: - assertValidStringClaim(entry.getKey(), jwt.getIssuer(), (String) entry.getValue()); + assertValidIssuerClaim(jwt.getIssuer(), (List) entry.getValue()); break; case PublicClaims.JWT_ID: assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue()); @@ -486,4 +485,10 @@ private void assertValidAudienceClaim(List audience, List value) throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); } } + + private void assertValidIssuerClaim(String issuer, List value) { + if (issuer == null || !value.contains(issuer)) { + throw new InvalidClaimException("The Claim 'iss' value doesn't match the required issuer."); + } + } } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index a38468ab..41b1ce3e 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -5,7 +5,7 @@ import java.util.Date; public interface Verification { - Verification withIssuer(String issuer); + Verification withIssuer(String... issuer); Verification withSubject(String subject); diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index d7bcdd2c..e297c59c 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -51,10 +51,22 @@ public void shouldValidateIssuer() throws Exception { assertThat(jwt, is(notNullValue())); } + @Test + public void shouldValidateMultipleIssuers() { + String auth0Token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + String otherIssuertoken = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJvdGhlcklzc3VlciJ9.k4BCOJJl-c0_Y-49VD_mtt-u0QABKSV5i3W-RKc74co"; + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("otherIssuer", "auth0") + .build(); + + assertThat(verifier.verify(auth0Token), is(notNullValue())); + assertThat(verifier.verify(otherIssuertoken), is(notNullValue())); + } + @Test public void shouldThrowOnInvalidIssuer() throws Exception { exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'iss' value doesn't match the required one."); + exception.expectMessage("The Claim 'iss' value doesn't match the required issuer."); String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; JWTVerifier.init(Algorithm.HMAC256("secret")) .withIssuer("invalid") @@ -62,6 +74,18 @@ public void shouldThrowOnInvalidIssuer() throws Exception { .verify(token); } + @Test + public void shouldThrowOnNullIssuer() throws Exception { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'iss' value doesn't match the required issuer."); + + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("auth0") + .build() + .verify(token); + } + @Test public void shouldValidateSubject() throws Exception { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; @@ -126,6 +150,18 @@ public void shouldThrowOnInvalidAudience() throws Exception { .verify(token); } + @Test + public void shouldRemoveAudienceWhenPassingNull() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withAudience("John") + .withAudience(null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + } + @Test public void shouldThrowOnNullCustomClaimName() throws Exception { exception.expect(IllegalArgumentException.class); From 8946bfe2c6e67622a7254899f1920b7c6d89b7b2 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 14 Mar 2019 15:49:17 -0300 Subject: [PATCH 036/293] Release 3.8.0 (#325) --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e951218b..c83a5cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.8.0](https://github.com/auth0/java-jwt/tree/3.8.0) (2019-03-14) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.7.0...3.8.0) + +**Added** +- Support multiple issuers #246 [\#288](https://github.com/auth0/java-jwt/pull/288) ([itdevelopmentapps](https://github.com/itdevelopmentapps)) + ## [3.7.0](https://github.com/auth0/java-jwt/tree/3.7.0) (2019-01-29) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.6.0...3.7.0) diff --git a/README.md b/README.md index f584a2d8..dd0da007 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.7.0 + 3.8.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.7.0' +implementation 'com.auth0:java-jwt:3.8.0' ``` ## Available Algorithms From aba456be6e63f4133741684b9783dc57f4bebede Mon Sep 17 00:00:00 2001 From: Ervis Zyka Date: Tue, 26 Mar 2019 23:12:32 +0200 Subject: [PATCH 037/293] fix javadoc parameter names (#327) --- lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index 93e8f176..dc92ff97 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -32,8 +32,8 @@ boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, * * @param algorithm algorithm name. * @param secretBytes algorithm secret. - * @param header JWT header. - * @param payload JWT payload. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. * @param signatureBytes JWT signature. * @return true if signature is valid. * @throws NoSuchAlgorithmException if the algorithm is not supported. From 890538970a9699b7251a4bfe4f26e8d7605ad530 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 25 Apr 2019 14:14:17 -0300 Subject: [PATCH 038/293] fix coverage badge (#330) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd0da007..bf61de4d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Java JWT [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) -[![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt/v3.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) +[![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). From e8f5db68a33b45fd1528872d1f35625dbd3e5006 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 3 May 2019 14:39:15 -0700 Subject: [PATCH 039/293] Setup the CODEOWNERS for pull request reviews (#331) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..6a263cd8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @auth0/sdk-team-approvers From f1ba5add078f3449ced6972d79f75a9fd39dad24 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 22 May 2019 15:29:17 -0600 Subject: [PATCH 040/293] bump dependencies (#337) --- lib/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 7126e324..6962e326 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,6 +1,6 @@ plugins { id "com.jfrog.bintray" version "1.8.4" - id "com.auth0.gradle.oss-library.java" version "0.8.0" + id "com.auth0.gradle.oss-library.java" version "0.9.0" id "jacoco" } @@ -34,8 +34,8 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - implementation 'commons-codec:commons-codec:1.11' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' + implementation 'commons-codec:commons-codec:1.12' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' testImplementation 'net.jodah:concurrentunit:0.4.3' From 39a9f74a536386bdd1b43468552d28cdbe409e14 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 23 May 2019 09:54:32 -0600 Subject: [PATCH 041/293] Release 3.8.1 (#338) --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83a5cfd..1761e4c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.8.1](https://github.com/auth0/java-jwt/tree/3.8.1) (2019-05-22) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.0...3.8.1) + +**Security** +- Bump dependencies and fix security issue [\#337](https://github.com/auth0/java-jwt/pull/337) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [3.8.0](https://github.com/auth0/java-jwt/tree/3.8.0) (2019-03-14) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.7.0...3.8.0) diff --git a/README.md b/README.md index bf61de4d..5856424b 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.8.0 + 3.8.1 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.8.0' +implementation 'com.auth0:java-jwt:3.8.1' ``` ## Available Algorithms From 4dca9515c7c742b727dc71968174210241eea88d Mon Sep 17 00:00:00 2001 From: Daniel Brodsky Date: Mon, 15 Jul 2019 16:00:52 -0700 Subject: [PATCH 042/293] Fix: updated jackson-databind to 2.9.9.1 to block CVE-2019-12814 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 6962e326..4d2064f1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' implementation 'commons-codec:commons-codec:1.12' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From 0b2c8a716ca110c13ba07a0d784d446ad7ba36a6 Mon Sep 17 00:00:00 2001 From: Daniel Brodsky Date: Thu, 8 Aug 2019 19:48:17 -0700 Subject: [PATCH 043/293] Fix: updated jackson-databind to 2.9.9.3 blocks CVE-2019-12814, CVE-2019-14379 and CVE-2019-14439 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 4d2064f1..d3659a3a 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3' implementation 'commons-codec:commons-codec:1.12' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From e4811e5baf0af36de902cc2df95e084a444408a8 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 15 Aug 2019 13:13:48 -0500 Subject: [PATCH 044/293] Release 3.8.2 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1761e4c9..d2333dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.8.2](https://github.com/auth0/java-jwt/tree/3.8.2) (2019-08-15) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.1...3.8.2) + +**Security** +- Fix: updated jackson-databind to 2.9.9.3 to block CVE [\#347](https://github.com/auth0/java-jwt/pull/347) ([danbrodsky](https://github.com/danbrodsky)) + ## [3.8.1](https://github.com/auth0/java-jwt/tree/3.8.1) (2019-05-22) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.0...3.8.1) diff --git a/README.md b/README.md index 5856424b..56e903ad 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.8.1 + 3.8.2 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.8.1' +implementation 'com.auth0:java-jwt:3.8.2' ``` ## Available Algorithms From 7adb0cfaf6516d41b36f749ac869f788a1b27f7b Mon Sep 17 00:00:00 2001 From: Daniel Brodsky Date: Fri, 20 Sep 2019 11:44:36 -0700 Subject: [PATCH 045/293] Fix: updated jackson-databind to 2.10.0.pr3 to block CVE-2019-14540 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index d3659a3a..4cb57f61 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0.pr3' implementation 'commons-codec:commons-codec:1.12' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From 68f9cf5bff96536967978bfdaa6743d5ce098e5e Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 25 Sep 2019 10:09:03 -0500 Subject: [PATCH 046/293] Release 3.8.3 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2333dfe..61695acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.8.3](https://github.com/auth0/java-jwt/tree/3.8.3) (2019-09-25) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.2...3.8.3) + +**Security** +- Fix: updated jackson-databind to 2.10.0.pr3 to block CVE [\#356](https://github.com/auth0/java-jwt/pull/356) ([danbrodsky](https://github.com/danbrodsky)) + ## [3.8.2](https://github.com/auth0/java-jwt/tree/3.8.2) (2019-08-15) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.1...3.8.2) diff --git a/README.md b/README.md index 56e903ad..b3affec5 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.8.2 + 3.8.3 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.8.2' +implementation 'com.auth0:java-jwt:3.8.3' ``` ## Available Algorithms From 67d5d228e2a19c7b1805da45e9a46cb5fb050d40 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 8 Oct 2019 10:18:19 -0700 Subject: [PATCH 047/293] Setup the CODEOWNERS for pull request reviews --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6a263cd8..073d5ffc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/sdk-team-approvers +* @auth0/dx-sdk-approver From 12450227ed5d517f56c0d33643065fbdba767308 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 8 Oct 2019 12:08:33 -0700 Subject: [PATCH 048/293] Setup the CODEOWNERS for pull request reviews --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 073d5ffc..c9ff4921 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdk-approver +* @auth0/dx-sdks-approver From 9d6968292f886f4054ddd313c50754b9e623269e Mon Sep 17 00:00:00 2001 From: Josh Cunningham Date: Fri, 25 Oct 2019 17:03:23 -0700 Subject: [PATCH 049/293] Setup the .github/stale.yml for Probot:Stale --- .github/stale.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..b2e13fc7 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 90 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +daysUntilClose: 7 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: [] + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: true + +# Label to use when marking as stale +staleLabel: closed:stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ \ No newline at end of file From c42476df837b54c20bb5291ba4e265d057cf7914 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 5 Nov 2019 15:12:37 +0000 Subject: [PATCH 050/293] fixing pr changes request not real code change just test fixing to remove confusions in the test readability --- .../main/java/com/auth0/jwt/JWTCreator.java | 5 ++-- .../java/com/auth0/jwt/JWTCreatorTest.java | 29 +++++++++++++++---- .../test/java/com/auth0/jwt/JsonMatcher.java | 4 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 4ab9906f..f7f15602 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -67,14 +67,15 @@ public static class Builder { /** * Add specific Claims to set as the Header. * If provided map is null then nothing is changed - * If provided map contains a header with null value then that header will be removed from the header claims + * If provided map contains a claim with null value then that claim will be removed from the header * * @param headerClaims the values to use as Claims in the token's Header. * @return this same Builder instance. */ public Builder withHeader(Map headerClaims) { - if (headerClaims == null) + if (headerClaims == null) { return this; + } for (Map.Entry entry : headerClaims.entrySet()) { if (entry.getValue() == null) { diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index f2a54803..47ba0f05 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -1,6 +1,7 @@ package com.auth0.jwt; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; import org.apache.commons.codec.binary.Base64; @@ -65,34 +66,50 @@ public void shouldReturnBuilderIfNullMapIsProvided() throws Exception { @Test public void shouldOverwriteExistingIfHeadersMapContainsTheSameKey() throws Exception { Map header = new HashMap(); - header.put("test", 456); + header.put(PublicClaims.KEY_ID, "xyz"); String signed = JWTCreator.init() - .withClaim("test", 123) + .withKeyId("abc") .withHeader(header) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.hasEntry("test", 456)); + assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); + } + + @Test + public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() throws Exception { + Map header = new HashMap(); + header.put(PublicClaims.KEY_ID, "xyz"); + + String signed = JWTCreator.init() + .withHeader(header) + .withKeyId("abc") + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "abc")); } @Test public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { Map header = new HashMap(); - header.put("test", null); + header.put(PublicClaims.KEY_ID, null); header.put("test2", "isSet"); String signed = JWTCreator.init() - .withClaim("test", 123) + .withKeyId("test") .withHeader(header) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.hasEntry("test", null)); + assertThat(headerJson, JsonMatcher.isNotPresent(PublicClaims.KEY_ID)); assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet")); } diff --git a/lib/src/test/java/com/auth0/jwt/JsonMatcher.java b/lib/src/test/java/com/auth0/jwt/JsonMatcher.java index f03547d4..b09ab187 100644 --- a/lib/src/test/java/com/auth0/jwt/JsonMatcher.java +++ b/lib/src/test/java/com/auth0/jwt/JsonMatcher.java @@ -68,6 +68,10 @@ public static JsonMatcher hasEntry(String key, Matcher valueMatcher) { return new JsonMatcher(key, null, valueMatcher); } + public static JsonMatcher isNotPresent(String key) { + return new JsonMatcher(key, null, null); + } + private String getStringKey(String key) { return "\"" + key + "\":"; } From bb8371797dbe1d1119890110f22c4aeb8dd60d58 Mon Sep 17 00:00:00 2001 From: Maxim Balan Date: Tue, 5 Nov 2019 16:35:20 +0000 Subject: [PATCH 051/293] Update lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java Co-Authored-By: Luciano Balmaceda --- lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 47ba0f05..05dd1e30 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -64,7 +64,7 @@ public void shouldReturnBuilderIfNullMapIsProvided() throws Exception { } @Test - public void shouldOverwriteExistingIfHeadersMapContainsTheSameKey() throws Exception { + public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() throws Exception { Map header = new HashMap(); header.put(PublicClaims.KEY_ID, "xyz"); @@ -417,4 +417,4 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { String[] parts = jwt.split("\\."); assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); } -} \ No newline at end of file +} From d89d746c1a10c128d257a3ef6187bd72937cc0c8 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 20 Dec 2019 17:05:29 -0600 Subject: [PATCH 052/293] Support serialization of DecodedJWT --- .../main/java/com/auth0/jwt/JWTDecoder.java | 5 ++- .../java/com/auth0/jwt/impl/BasicHeader.java | 5 ++- .../java/com/auth0/jwt/impl/PayloadImpl.java | 6 ++- .../java/com/auth0/jwt/JWTDecoderTest.java | 42 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index 34503f43..b14c2ac3 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -9,6 +9,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.StringUtils; +import java.io.Serializable; import java.util.Date; import java.util.List; import java.util.Map; @@ -17,7 +18,9 @@ * The JWTDecoder class holds the decode method to parse a given JWT token into it's JWT representation. */ @SuppressWarnings("WeakerAccess") -final class JWTDecoder implements DecodedJWT { +final class JWTDecoder implements DecodedJWT, Serializable { + + private static final long serialVersionUID = 1873362438023312895L; private final String[] parts; private final Header header; diff --git a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java index b1d81a11..0a782268 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java +++ b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectReader; +import java.io.Serializable; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -14,7 +15,9 @@ /** * The BasicHeader class implements the Header interface. */ -class BasicHeader implements Header { +class BasicHeader implements Header, Serializable { + private static final long serialVersionUID = -4659137688548605095L; + private final String algorithm; private final String type; private final String contentType; diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index d3b1dc0c..2c5558f2 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectReader; +import java.io.Serializable; import java.util.*; import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim; @@ -12,7 +13,10 @@ /** * The PayloadImpl class implements the Payload interface. */ -class PayloadImpl implements Payload { +class PayloadImpl implements Payload, Serializable { + + private static final long serialVersionUID = 1659021498824562311L; + private final String issuer; private final String subject; private final List audience; diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index e164dc07..dfe59182 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -12,6 +12,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; @@ -289,6 +290,34 @@ public void shouldGetAvailableClaims() throws Exception { assertThat(jwt.getClaims().get("extraClaim"), is(notNullValue())); } + @Test + public void shouldSerializeAndDeserialize() throws Exception { + DecodedJWT originalJwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwibmJmIjoxMjM0NTY3ODkwLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.2_0nxDPJwOk64U5V5V9pt8U92jTPJbGsHYQ35HYhbdE"); + + assertThat(originalJwt, is(instanceOf(Serializable.class))); + + byte[] serialized = serialize(originalJwt); + DecodedJWT deserializedJwt = (DecodedJWT) deserialize(serialized); + + assertThat(originalJwt.getHeader(), is(equalTo(deserializedJwt.getHeader()))); + assertThat(originalJwt.getPayload(), is(equalTo(deserializedJwt.getPayload()))); + assertThat(originalJwt.getSignature(), is(equalTo(deserializedJwt.getSignature()))); + assertThat(originalJwt.getToken(), is(equalTo(deserializedJwt.getToken()))); + assertThat(originalJwt.getAlgorithm(), is(equalTo(deserializedJwt.getAlgorithm()))); + assertThat(originalJwt.getAudience(), is(equalTo(deserializedJwt.getAudience()))); + assertThat(originalJwt.getContentType(), is(equalTo(deserializedJwt.getContentType()))); + assertThat(originalJwt.getExpiresAt(), is(equalTo(deserializedJwt.getExpiresAt()))); + assertThat(originalJwt.getId(), is(equalTo(deserializedJwt.getId()))); + assertThat(originalJwt.getIssuedAt(), is(equalTo(deserializedJwt.getIssuedAt()))); + assertThat(originalJwt.getIssuer(), is(equalTo(deserializedJwt.getIssuer()))); + assertThat(originalJwt.getKeyId(), is(equalTo(deserializedJwt.getKeyId()))); + assertThat(originalJwt.getNotBefore(), is(equalTo(deserializedJwt.getNotBefore()))); + assertThat(originalJwt.getSubject(), is(equalTo(deserializedJwt.getSubject()))); + assertThat(originalJwt.getType(), is(equalTo(deserializedJwt.getType()))); + assertThat(originalJwt.getClaims().get("extraClaim").asString(), + is(equalTo(deserializedJwt.getClaims().get("extraClaim").asString()))); + } + //Helper Methods private DecodedJWT customJWT(String jsonHeader, String jsonPayload, String signature) { @@ -297,4 +326,17 @@ private DecodedJWT customJWT(String jsonHeader, String jsonPayload, String signa return JWT.decode(String.format("%s.%s.%s", header, body, signature)); } + private static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + ObjectOutputStream o = new ObjectOutputStream(b); + o.writeObject(obj); + return b.toByteArray(); + } + + private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException { + ByteArrayInputStream b = new ByteArrayInputStream(bytes); + ObjectInputStream o = new ObjectInputStream(b); + return o.readObject(); + } + } \ No newline at end of file From 18e8b8bc84ea8f541a58104ad48086a827f6e258 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 2 Jan 2020 12:08:35 -0600 Subject: [PATCH 053/293] Release 3.9.0 --- CHANGELOG.md | 9 +++++++++ README.md | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61695acf..9c819ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [3.9.0](https://github.com/auth0/java-jwt/tree/3.9.0) (2020-01-02) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.3...3.9.0) + +**Added** +- Support serialization of DecodedJWT [\#370](https://github.com/auth0/java-jwt/pull/370) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Fixing JwtCreator builder when setting headers as a map [\#320](https://github.com/auth0/java-jwt/pull/320) ([maxbalan](https://github.com/maxbalan)) + ## [3.8.3](https://github.com/auth0/java-jwt/tree/3.8.3) (2019-09-25) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.2...3.8.3) diff --git a/README.md b/README.md index b3affec5..bbee8607 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,14 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o com.auth0 java-jwt - 3.8.3 + 3.9.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.8.3' +implementation 'com.auth0:java-jwt:3.9.0' ``` ## Available Algorithms From d9cebe1b4652ef53e47a1b3602ca0db2232b18c2 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 23 Jan 2020 14:32:19 -0600 Subject: [PATCH 054/293] Move Javadoc to Verification interface --- .../main/java/com/auth0/jwt/JWTVerifier.java | 132 +----------------- .../auth0/jwt/interfaces/Verification.java | 131 +++++++++++++++++ 2 files changed, 132 insertions(+), 131 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index a1c7f82b..ff25db09 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -39,9 +39,6 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { return new BaseVerification(algorithm); } - /** - * The Verification class holds the Claims required by a JWT to be valid. - */ public static class BaseVerification implements Verification { private final Algorithm algorithm; private final Map claims; @@ -58,50 +55,24 @@ public static class BaseVerification implements Verification { this.defaultLeeway = 0; } - /** - * Require a specific Issuer ("iss") claim. - * - * @param issuer the required Issuer value. If multiple values are given, the claim must at least match one of them - * @return this same Verification instance. - */ @Override public Verification withIssuer(String... issuer) { requireClaim(PublicClaims.ISSUER, issuer == null ? null : Arrays.asList(issuer)); return this; } - /** - * Require a specific Subject ("sub") claim. - * - * @param subject the required Subject value - * @return this same Verification instance. - */ @Override public Verification withSubject(String subject) { requireClaim(PublicClaims.SUBJECT, subject); return this; } - /** - * Require a specific Audience ("aud") claim. - * - * @param audience the required Audience value - * @return this same Verification instance. - */ @Override public Verification withAudience(String... audience) { requireClaim(PublicClaims.AUDIENCE, audience == null ? null : Arrays.asList(audience)); return this; } - /** - * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - * Setting a specific leeway value on a given Claim will override this value for that Claim. - * - * @param leeway the window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptLeeway(long leeway) throws IllegalArgumentException { assertPositive(leeway); @@ -109,14 +80,6 @@ public Verification acceptLeeway(long leeway) throws IllegalArgumentException { return this; } - /** - * Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. - * Expiration Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Expires At Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); @@ -124,14 +87,6 @@ public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException return this; } - /** - * Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. - * Not Before Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Not Before Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); @@ -139,14 +94,6 @@ public Verification acceptNotBefore(long leeway) throws IllegalArgumentException return this; } - /** - * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. - * Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Issued At Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); @@ -154,34 +101,18 @@ public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException return this; } - /** - * Skip the Issued At ("iat") date verification. By default, the verification is performed. - */ + @Override public Verification ignoreIssuedAt() { this.ignoreIssuedAt = true; return this; } - /** - * Require a specific JWT Id ("jti") claim. - * - * @param jwtId the required Id value - * @return this same Verification instance. - */ @Override public Verification withJWTId(String jwtId) { requireClaim(PublicClaims.JWT_ID, jwtId); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); @@ -189,14 +120,6 @@ public Verification withClaim(String name, Boolean value) throws IllegalArgument return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Integer value) throws IllegalArgumentException { assertNonNull(name); @@ -204,14 +127,6 @@ public Verification withClaim(String name, Integer value) throws IllegalArgument return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Long value) throws IllegalArgumentException { assertNonNull(name); @@ -219,14 +134,6 @@ public Verification withClaim(String name, Long value) throws IllegalArgumentExc return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Double value) throws IllegalArgumentException { assertNonNull(name); @@ -234,14 +141,6 @@ public Verification withClaim(String name, Double value) throws IllegalArgumentE return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, String value) throws IllegalArgumentException { assertNonNull(name); @@ -249,14 +148,6 @@ public Verification withClaim(String name, String value) throws IllegalArgumentE return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Date value) throws IllegalArgumentException { assertNonNull(name); @@ -264,14 +155,6 @@ public Verification withClaim(String name, Date value) throws IllegalArgumentExc return this; } - /** - * Require a specific Array Claim to contain at least the given items. - * - * @param name the Claim's name. - * @param items the items the Claim must contain. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { assertNonNull(name); @@ -279,14 +162,6 @@ public Verification withArrayClaim(String name, String... items) throws IllegalA return this; } - /** - * Require a specific Array Claim to contain at least the given items. - * - * @param name the Claim's name. - * @param items the items the Claim must contain. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException { assertNonNull(name); @@ -294,11 +169,6 @@ public Verification withArrayClaim(String name, Integer... items) throws Illegal return this; } - /** - * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. - * - * @return a new JWTVerifier instance. - */ @Override public JWTVerifier build() { return this.build(new ClockImpl()); diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 41b1ce3e..511e59b7 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -4,40 +4,171 @@ import java.util.Date; +/** + * Holds the Claims and claim-based configurations required for a JWT to be considered valid. + */ public interface Verification { + /** + * Require a specific Issuer ("iss") claim. + * + * @param issuer the required Issuer value. If multiple values are given, the claim must at least match one of them + * @return this same Verification instance. + */ Verification withIssuer(String... issuer); + /** + * Require a specific Subject ("sub") claim. + * + * @param subject the required Subject value + * @return this same Verification instance. + */ Verification withSubject(String subject); + /** + * Require a specific Audience ("aud") claim. + * + * @param audience the required Audience value + * @return this same Verification instance. + */ Verification withAudience(String... audience); + /** + * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. + * Setting a specific leeway value on a given Claim will override this value for that Claim. + * + * @param leeway the window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptLeeway(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. + * Expiration Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * + * @param leeway the window in seconds in which the Expires At Claim will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptExpiresAt(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. + * Not Before Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * + * @param leeway the window in seconds in which the Not Before Claim will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptNotBefore(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. + * Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * + * @param leeway the window in seconds in which the Issued At Claim will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptIssuedAt(long leeway) throws IllegalArgumentException; + /** + * Require a specific JWT Id ("jti") claim. + * + * @param jwtId the required Id value + * @return this same Verification instance. + */ Verification withJWTId(String jwtId); + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, Boolean value) throws IllegalArgumentException; + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, Integer value) throws IllegalArgumentException; + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, Long value) throws IllegalArgumentException; + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, Double value) throws IllegalArgumentException; + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, String value) throws IllegalArgumentException; + /** + * Require a specific Claim value. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withClaim(String name, Date value) throws IllegalArgumentException; + /** + * Require a specific Array Claim to contain at least the given items. + * + * @param name the Claim's name. + * @param items the items the Claim must contain. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withArrayClaim(String name, String... items) throws IllegalArgumentException; + /** + * Require a specific Array Claim to contain at least the given items. + * + * @param name the Claim's name. + * @param items the items the Claim must contain. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; + /** + * Skip the Issued At ("iat") date verification. By default, the verification is performed. + */ Verification ignoreIssuedAt(); + /** + * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. + * + * @return a new JWTVerifier instance. + */ JWTVerifier build(); } From a3e8a6b766253dcd548c79cff2c06594519abcb5 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 23 Jan 2020 16:19:01 -0600 Subject: [PATCH 055/293] Update CHANGELOG for breaking changes in 3.4.0 --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c819ea0..e03dd703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,8 +69,15 @@ ## [3.4.0](https://github.com/auth0/java-jwt/tree/3.4.0) (2018-06-13) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.3.0...3.4.0) +**Breaking Changes** +- Fix for [\#236](https://github.com/auth0/java-jwt/pull/236) - refactored HMACAlgorithm so that it doesn't throw an UnsupportedEncodingException [\#242](https://github.com/auth0/java-jwt/pull/242) ([obecker](https://github.com/obecker)). + +Clients using the following methods may need to update their code to not catch an `UnsupportedEncodingException`: +- `public static Algorithm HMAC384(String secret)` +- `public static Algorithm HMAC256(String secret)` +- `public static Algorithm HMAC512(String secret)` + **Changed** -- Fix for issue #236 - refactored HMACAlgorithm so that it doesn't throw an UnsupportedEncodingException [\#242](https://github.com/auth0/java-jwt/pull/242) ([obecker](https://github.com/obecker)) - Throw JWTDecodeException when date claim format is invalid [\#241](https://github.com/auth0/java-jwt/pull/241) ([lbalmaceda](https://github.com/lbalmaceda)) **Security** From e88b98b78b6a9082cd31f95c950fa4e42bf53fdb Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 24 Jan 2020 13:21:16 -0600 Subject: [PATCH 056/293] Link to gist demonstrating how to read keys --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bbee8607..104793cf 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ RSAPrivateKey privateKey = //Get the key instance Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); ``` +> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, you can see the example [here](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). + #### Using a KeyProvider: By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: @@ -138,7 +140,6 @@ try { If a Claim couldn't be converted to JSON or the Key used in the signing process was invalid a `JWTCreationException` will raise. - ### Verify a Token You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` and passing the `Algorithm` instance. If you require the token to have specific Claim values, use the builder to define them. The instance returned by the method `build()` is reusable, so you can define it once and use it to verify different tokens. Finally call `verifier.verify()` passing the token. From 405cc605136addfce4eaa635844a3d6e71d90679 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 24 Jan 2020 13:27:37 -0600 Subject: [PATCH 057/293] whitespace fix --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 104793cf..bc1c44a7 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ try { If a Claim couldn't be converted to JSON or the Key used in the signing process was invalid a `JWTCreationException` will raise. + ### Verify a Token You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` and passing the `Algorithm` instance. If you require the token to have specific Claim values, use the builder to define them. The instance returned by the method `build()` is reusable, so you can define it once and use it to verify different tokens. Finally call `verifier.verify()` passing the token. From 3cca8c009e72a9a2dcf2a6d21521980dc73faf0d Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 24 Jan 2020 14:03:58 -0600 Subject: [PATCH 058/293] Update wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc1c44a7..d071b958 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ RSAPrivateKey privateKey = //Get the key instance Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); ``` -> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, you can see the example [here](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). +> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). #### Using a KeyProvider: From 1b5ef2914a4de3e21819f475488f7d76af1bcd81 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 3 Feb 2020 13:29:56 -0300 Subject: [PATCH 059/293] allow to customize the typ header claim --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 4 +++- .../test/java/com/auth0/jwt/JWTCreatorTest.java | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index f7f15602..deda9813 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -316,7 +316,9 @@ public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCrea throw new IllegalArgumentException("The Algorithm cannot be null."); } headerClaims.put(PublicClaims.ALGORITHM, algorithm.getName()); - headerClaims.put(PublicClaims.TYPE, "JWT"); + if (!headerClaims.containsKey(PublicClaims.TYPE)) { + headerClaims.put(PublicClaims.TYPE, "JWT"); + } String signingKeyId = algorithm.getSigningKeyId(); if (signingKeyId != null) { withKeyId(signingKeyId); diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 05dd1e30..471ff38b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -292,7 +293,7 @@ public void shouldSetCorrectAlgorithmInTheHeader() throws Exception { } @Test - public void shouldSetCorrectTypeInTheHeader() throws Exception { + public void shouldSetDefaultTypeInTheHeader() throws Exception { String signed = JWTCreator.init() .sign(Algorithm.HMAC256("secret")); @@ -302,6 +303,19 @@ public void shouldSetCorrectTypeInTheHeader() throws Exception { assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); } + @Test + public void shouldSetCustomTypeInTheHeader() throws Exception { + Map header = Collections.singletonMap("typ", "passport"); + String signed = JWTCreator.init() + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("typ", "passport")); + } + @Test public void shouldSetEmptySignatureIfAlgorithmIsNone() throws Exception { String signed = JWTCreator.init() From 9742669962c3bc1c0153a39b19e574fad94a704a Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 3 Feb 2020 13:55:01 -0300 Subject: [PATCH 060/293] add javadoc url and badge --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index d071b958..fcb27e2d 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) +[![Javadoc](https://javadoc.io/badge2/com.auth0/java-jwt/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). @@ -12,6 +13,8 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o ## Installation +The library is available on both Maven Central and Bintray, and the Javadoc published [here](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html). + ### Maven ```xml From 99195bf74825166bb85893d6687a6ef5362803ad Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Mon, 3 Feb 2020 14:48:15 -0300 Subject: [PATCH 061/293] Update README.md Co-Authored-By: Rita Zerrizuela --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcb27e2d..b63599a2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o ## Installation -The library is available on both Maven Central and Bintray, and the Javadoc published [here](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html). +The library is available on both Maven Central and Bintray, and the Javadoc is published [here](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html). ### Maven From 0a8e50e6be63265bc698005b3e33f05b137e81ac Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 12 Feb 2020 15:12:56 -0600 Subject: [PATCH 062/293] Update tests to use valid Base64 URL-encoded tokens --- .../jwt/algorithms/ECDSAAlgorithmTest.java | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 8968ed9f..b50832d1 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -21,7 +21,6 @@ import java.security.interfaces.ECPublicKey; import java.util.Arrays; - import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; @@ -75,7 +74,7 @@ public void shouldThrowOnECDSA256VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); Algorithm algorithm = Algorithm.ECDSA256(key); algorithm.verify(JWT.decode(jwt)); @@ -95,7 +94,7 @@ public void shouldThrowOnECDSA256VerificationWithDERSignatureWithBothKeys() thro exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -200,7 +199,7 @@ public void shouldThrowOnECDSA384VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UtccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); Algorithm algorithm = Algorithm.ECDSA384(key); algorithm.verify(JWT.decode(jwt)); @@ -220,7 +219,7 @@ public void shouldThrowOnECDSA384VerificationWithDERSignatureWithBothKeys() thro exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -325,7 +324,7 @@ public void shouldThrowOnECDSA512VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); Algorithm algorithm = Algorithm.ECDSA512(key); algorithm.verify(JWT.decode(jwt)); @@ -345,7 +344,7 @@ public void shouldThrowECDSA512VerificationWithDERSignatureWithBothKeys() throws exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -519,7 +518,7 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception private static final byte[] ES512HeaderBytes = ES512Header.getBytes(StandardCharsets.UTF_8); private static final byte[] auth0IssPayloadBytes = auth0IssPayload.getBytes(StandardCharsets.UTF_8); - + @Test public void shouldDoECDSA256Signing() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); @@ -549,7 +548,7 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA256(provider); - + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); assertSignaturePresent(jwt); @@ -607,7 +606,7 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA384(provider); - + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); assertSignaturePresent(jwt); @@ -642,7 +641,7 @@ public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { public void shouldDoECDSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - + String jwt = asJWT(algorithmSign, ES512Header, auth0IssPayload); assertSignaturePresent(jwt); @@ -652,7 +651,7 @@ public void shouldDoECDSA512Signing() throws Exception { @Test public void shouldDoECDSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); assertSignaturePresent(jwt); @@ -668,7 +667,7 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA512(provider); - + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); assertSignaturePresent(jwt); @@ -843,7 +842,7 @@ public void shouldSignAndVerifyWithECDSA256() throws Exception { public void shouldSignAndVerifyWithECDSA384() throws Exception { ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); String header384 = "eyJhbGciOiJFUzM4NCJ9"; - String body = "eyJpc3MiOiJhdXRoMCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { String jwt = asJWT(algorithm384, header384, body); @@ -855,7 +854,7 @@ public void shouldSignAndVerifyWithECDSA384() throws Exception { public void shouldSignAndVerifyWithECDSA512() throws Exception { ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); String header512 = "eyJhbGciOiJFUzUxMiJ9"; - String body = "eyJpc3MiOiJhdXRoMCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { String jwt = asJWT(algorithm512, header512, body); @@ -1171,7 +1170,7 @@ public void shouldBeEqualSignatureMethodDecodeResults() throws Exception { /** * Test deprecated signing method error handling. - * + * * @see {@linkplain #shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull} * @throws Exception expected exception */ From 0908db724a240974874c06b40f7025c3950d50aa Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 12 Feb 2020 15:39:00 -0600 Subject: [PATCH 063/293] Update to Gradle 6.1.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 18 +++++++++++++++++- gradlew.bat | 18 +++++++++++++++++- lib/build.gradle | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch delta 47360 zcmY(oV{qSJ6z!dcjcwbuZQD*`yV19??WD17+l_78_{B++^!eX=pShShXU=)EXZDLd zYxepqP%A`#BLtL+JOm_MA_y}P4;>v24D9=NFfcGtFoy;qL6QG{!ige^=yW02lvo(W zSRhxB>o>6fUC@T~?SB?-V*Zbp4dee*SFT&GK|q0lUBD*akl-e(d?N(3(X}zY;xa8v z2%yYGf}?`D(U>AzR3uFyHmvf8ZLZx| zUj2&xiWahY$s89!3#wvR$z=a~wfS=G|9o_7)h7()3@1zzaS#;rO<}@YepC{wr@eTO zt(GQZP_sdS{yS-r33L-+*0B5L3<|H6P8k0HiW~(tZn12a$U<$5DQMl+CX@f+ zy-_yFdUWRUr0@pkpob}COS5P&VQNi@)zJMhXngU7u*cv;={*Q==)z1lvUDGs=h^Mf`F#^gdV)%T{zfJi0h@9K{H;ju|`w%D#9SW_ouwvSydBoL4KkAagmu~T$rLemehIG z6K$X&&@Vorf2R9!7$eE6>qM3#mQ3{0e?&`HQW!9#>_jgPH@V>y7;-M zgVo_*df?_)a3Jp|Y5iF&6?8|;-upBe>9%msd!28*1&(7L}VMf41FZb)z!Xa<$fhJ7kM%rGLik}6C_WpGnBK&Q; z6z>$xmPQ=+WNhJ*TWi5SDUH~WXaxBv#ByW%R@P>&7iN-9=cc(rU8B*va{sMAxL13S zL=b4!^KQ%NTJ;Fm=U`77nEFoUU``1wEp9he$7cXg!+9Q8#W`bik7W(Mt0nq{b|pKE zN*W(P?ei&ate%d5hF45mpiBt$RC2jD*BBfcWP7dWBoHoh{nI<&?TeWI8F9Q-Mknb@ zYAkiPvm!|AL(Aby5ZT%qxGo$pZjYD#)6NL*drfX}F{h#g}0!KpZ((M(I9@ zKTt{UFU{+>K^K&v$9dt{0E0nO29Y6$LA`wR#1Bm20iSy_?if^Llrb7LND8q&{Do%t z#1W|9!}1u%95rQkY=Kxp%7>T>eClO`!oI09M&z=>Yi@8b9HKqU}C^NAPy?|XU-u+CPfap9?2{y_0ESji% zf6rXvVBeTX_NwgkpTIzYL9_6lEn)a-{^xnB*4(0e1#A)rxS(9U^1>Iw0G7LTaZv$2 zI;4)Zq7w@H_56TX(1ve?q(P-z855 zkzge|Pkm2bheii)p-aAjCI)a%5RrRdjBdx!`|-q~M_EWHtbE-vx3KllM)fyw93*=g zMhsD?_>*le;fvxLdpCZQl1^2t8}KIDjpI{S%JF?oGHQj)58#}0>3K5?k~&niV@ZJ) zOHyS#Mi9*K)=6?#S+&o5;p;Grek%BY|XEzT)za84?* z=jgr1E6hn4hgcsV-$~=%rUW5!NWPd_o$R>H2zoi5tlr)Vf7==}he6Nq*fU!hHGq3S zax^0i9l=POdQJ=evE`ZY%gKCXlrRirWlC^yiU2FzHv%LuOlFz1>%f|WI(xdvm+*Vh zRfnto(8ag5!%cS(D_lse6>fzk`EIwgI!5S(Yu1*SIUA2Qs2oSM=>@TjL}@(b*LpLe ziAsYk)ywxnuZ9zkT1uM0DZ?r{=kO(NWi;{sgtA%cJU*npd_W+Z6$J0+C&hLlz<>Q2 ztfE|OX#un?GWcQs?AcGWRz^L|J?082Va6b1+bATB^5)`D;p=h3D=yw{rm1kY*3HPOjKmI@EML6dopR-Trf*F9h{Ps z6}w;>YBa|EWsN!dr1oVWu|JNoC#=R^W^N%i=?Vl_ahC98%Dlf_vxz&(L|!*$-aSTe zu`1V%hQ6WIPmzc+|5Ex^!vPUfL(u;&hYyeY9{&;h{kBnT#8P{J9>;Oc)*AC+Zr z1A}(keMCCi?J_GQae>c7(EsL2c8ZF)M!l}={-uES7h<7<+y=aqbvZpiu4?&ZB<$}5 z(Kqh*-l-eDQr~8L!4HJmM>+i^AENaAy{y0=YQX;)qW?KVoUH=c{YYS7zX`#>NdNyC zLIQGhVf_robpHVhH@#)ci~COZ5lS$R7M-!e00iNesDl;lKSk`(k!cF0xB{eda z#vD#3*-j^2|Ja+}w%Ux|5q{;|uaK-9t^z@416EaQT?JXI8V{G1Z-|_KdC~iDhn@D@ z5dDNANCK!Mc1LcZKnMZGoIt+!mkK9*s2u!#e>TXQ$XH|1SZz8l z`!$+mC%#W(+8Fn>@%_sKWp>{w=vCiOk`vIDv;mwBh=X3GKav9hE|3pOHi$U@CR#JyKls1MBdkDqRA2I{U;5XNoRV)@fpzr$U(%fas8gKx$o*n4E6UAzRK=bDCg- zP{u)nn{d@Noses}q!ZV|ZyZel>S^r|b*(1eNy03GY4H_1B(L!cs2ayp^c6d%Q>IJp zS&u!{TeBAOxz;RYibxf~tLPJ*w`SUNC5r2cy|_k zD^IuP3cjqhosic%XE(90TibIotfPG#8CV;XRTeW9iUs)h5!XS@=5kFyersLdi_E_Q z>qmoARY$UfTDfC8QID~`{h{#pS;;OX;z~$78MxtObabTSnoFflbO-cWK`gHgrjF;w z=EGKxOW7z^o|}d;g7@?u(lN!6Bv{eU=Ir0jIU1GxY4^WFHp)u2gkX}>(Llw5D{a4W z_+f71D9v^PM5Tw&@EEuNf7TzH3H_^?1Vu?6+YKR$$+>tgTi<*sZfH&^rLSKTu1A-6 zq#u7Kv%UjEYIN!&M@d=!+$X)d>?`q9=!XrF&6f-ch}X{(&?6e?PgnEs)K}-fIZt$y zs`t{uu6kj|?C`H{CuAcjH<88;;?hjk%+2LEOXX?ln=Gbee>O+}N?H!*dQ|-dlSMPl zSw{!&-BYz8r|q!(O2-S1egn1J23pxlyf=Zc)aexnA2L3E20s)=GLbHlWt5-z8>ykIHW7!G|>2}et237(}HV*5&_xf>`GY@#{1^RG+au4}whm54*{LPgI^17oy zX}cDd3nm)vY7!+~44t3^Dimu{o>2ID%lm%eNKRofj?_t2p7p%p`i_6@nn@nx|%b_2VX zfg;0@*%VE#+FxQ7#c;|T*SW#)J5zXO(>NxTs5WaLPv2DrN#9i>?%qIe5KO-FD1&mW zWHSLh?NO$V(qC?q`k2BTP2G9Fv`o-yDj`6=kc~vgsQhk6>zM{7w2dc_J0l`Y80-B)5#FBhlqGxmna@e9dVb{F_Ui zKPFnhp7y-FE*lei5PccaiqzTRh&_NL`OBzUDTvGRkwVYa(iuvH$ktb z9-42Vp}lraL`(2EM;2Z-664OULiwvE~$2Yeoo^%{t-cd&sXs9gqFyl&sNisq}nn9QJ%7v zJ|u|QKAAREPPZho;=>AKr{$OuT-AByY@2IFp6z@4j9@jDc2t-w+1gD-%(kbPWm6I(hcnKE4Z!FuaK=dpnL_HdBznXu!sH^O6lh zQ0N?&UzcC)Jcx>)p%D1%s#>m{CfF(W;1uR1E>~qqixO{>!(B96PdUA!9r)81tD7*0 zc8vclX7ii}9Wb1C(HSgzDOWXfNFT1gym$!T{s?7e@i1jLDSd2t;D8^Ow*>$Up3__J zNjxlLjv>TjCj;Bsv=gc*hz8GrTuSVAl$r}KBDn`ozT5JeA2+ZN7M`-!WBRxP09tb zqRGcU{-o5AhF47(-lxp=9l8Ob^BO~tl8($qTf8=1s>hqdff*_{;QNZZr!~9N0W-3 zZyi`ajGj^?NVmH1EvT6w{1N3D{3u z@z$eot=T3ATHUEVqYUJ|$WDvGmzp-Bg}!ncp*OUqsd~djyr{tKQOrB3v-u$d9bR^A zL1V))o?oo#F6S$Lb{%Oythv#R6ds~|X*1*2t>=;%y;go;+*%molGu48eV4gtdMuP7 zmn}QJ`|M(8dG5l5G##-jZ;ek&T7Oi+PeM&@75?zO`l-TqK86zcl9dvzI;R3;y#|8K z7JK?GChEV}S=vBa@+*WljHE@6UIq3Frw=yk51;r^k&BP3(`$j^e6vQZ{k%_ zkfKL>5b;vu#hv&@wD44KdvdtsMV`Q-$C6cjwIECQ+#Nw0vie<= zZuJ!`44cmKjh#K*U(1FpCgVlN5dQ+_wLc~fYv}`>p8tSGXuk=2?q%zt8YP3F=)Cgq(*&AG{h1fx)+XQl zkJ`g;c4r|w(wF|u#Xzh@BF(rr3PvyyND;@)?Mt%`&*Q{3yvMQUbY|`dBFHo6l8i0# zL?Raw6H}g!vHsF_iE30ngy#un-e>5Ifw{x{T?Am2fjky=`gKtSNCJK*)2*4AN|g1- zt7YqT2YDSz<1FQPW5u((AjuJJ$z~ujqsyq;O!K1gA%L%uBtt zxSi3E^1N1_T#TPwL%KZeb0e)L>9RN4t$0a!^GxksbYz)zvN82bRe9%ltQ|r%cl}U5 zI{=-_c^3dNi|f53(ie!1LVUbs5z90}nRSVO)-J0E5seG0%59@Kj(}l^;I~HwUmGy5 z@I{QfwpA?nf2lv1;M)v5>A#(QS=&D`P;()D7mJC+Jq`>-uCjS9i;!MWyXq z&z=}6lKtT9LdgS}kjxD7{tz!}Uq@xpo!rjr zK1=Y7JbSP+=Z{NZO@ZNcmn}+N#twDn#bR#r2Knz`fYLL9Hdp978;@+*j`kdo zN)NL%F|d$onwN6u_%dy3&EqcjZB5^(G>R&Ek0N+OS)TZq`?5&N2fBIPBC5_bscoUU zXLvH1p(+4ti9-Hd6>HI)f#SHX33%+sbNv9HY)jf|W3?qN!GBJAmw){s<(womvp{6N zRCgfx{0@soY_4qjdCje|NR&s$jgtL7*Z^6mj$KraqGgZYRE}D-5Y=~Whkua z(k)oB`HOQ;WT4!ntuw-xBIj&c1*f^4;S(JVnT)-M3Cf*j_Y~y0jBDAtfEgo3izzyP zwchk^eo38u@jo6hOm8v^71mYf0>Wp07@3xXJp2pwA(ND`*h=v36*=tu|MI?}ow)i0 zB#LR6?GSh7;dzD=&28kWZ-fz9Y~H}HUmPOGmMfER=s8`0vH$a*HzgMSI%8R+;3N3n zWEBE2Z`wr5XFzOiY2GYRXJ&5S3)iH1C-C$Ewp5o&n&zI-DXrtLuj@b2TJ+z>(F*nb zPu@ae15Sjp-KH4iuUYHO!I1a#Cq&XgI-gKM;g|JTV^e048SR!M=~({vXq7K>W$UdZjbXgxtRzh^TRxP&bkiLMKkaSgpeXrUU0#$?vPA|FJDatx6`qgJQ>TH_p@;BamH-S*}A}{`$>~}uNCjZCwPr`tvX0?ERXfN62lr) zljL6YB_Szt1m&4xxH-e$`^c2tN;V3^Fm@jgJIwax_Yx!G{)bVm-eJr2sKVAp$!GE8 zH)E<`o5==ysa1tr4$c!cgY9@+*N&g(4tsR#6w^=34u+nqyVM}V8lCjxh#!*!VxoW+ z9eSzxjC_3D==3iVmJHa#`mO4NOJrru-Nq)URay-}WxfEt4^sN|>_HnntYC)}H&2r1ouDj}^qN(vda6g#n}j`MQ`MWX;H!}jt8 zE*R}j_(W3d$67sNJu822;--Ws?emef5ebG#A$oWyOsbKm`{7?(`ysNsZY7o*qZiok z$ZB!DVRuo_J|zrAr=HeBB_Vb-;ok2W1GVoe18*yi|9YqPcbGpYEUVV^Xi)?Xi90Sc zl@hKhV++{4vl((}E-a2f<17N$hRii2S-mowHV}28VF4y4Xk}1D-S`YpWIIWL#0S#Q z0WogRLeEz2}PKNWORJospcuov=Df5 z7Iuv-E%-U{DqQSzxvm0b?tjCo<&)ncHSK99uTrPV8+-Ya`?Ng^f5$(c6xa1|b0}{t zRR2K?D@~*C{g0o-owvhn5J>4u!F_(WMC!~JBL78IoFK{#>Ej(m5~#!}(tfMK9x_XF z`<`&@lToI7&ryNxKZ_WVYe-D8(DWCAqX+xDFjj%5>vtoc?CsZ)bAOZ!!g^=`dq=eVxT>6MZKqc zT2g%;&(mPc{9^qX3+-Y(fY6ZsETd-9(^D;C>xI+8YX_?%gwVW_9q7K3%g2U1Nmld@;T&ocSugPK96}f#| z9Z;_ESj*iL&XpR8_w0Fd__YVkjkGPMM^aWMa|Va8P$fc5*lkWy(w8l7Kbzd1!@k0b z*OvLO@6ddHl%O>d=}AIWLc+^gs66*b<;e#<-q}B8c>)p;;SnPtuyUAbqN9R0rqIq@ zg1dYMr+>t=dxVa2UGYcilS1kf6%L60wnWnsB&tU^I>r+T+zo;9KKMScN9&_pPzQ?V zcxn(47PFJdBD>c@RU5F)55R7PLt%Y5X_1moU65=;GuWv^q} z+C61)kX|AWidVal5GA`coazGkd5B4-Ap%eR9#5J~Rg-zA>}Zci($?1p>Mn3=chm2;y&L#_?{eMfGp)ySj0rw*v4)id~c2i;r}Mo<^`Gkn^?zE zHzGL5J0^0umoS7oX~jRcx0&Cp8=N`Am*#K^%2!{SWcN8~;-Jy4(w|e#GHe+9yUILf zdjTGoO*-`Vcd~qk)oXoq7?U4o)F5V1VySJh7`2TX?3I4U#W-OhaZkCjNDb0M|Liqc z4W+|vyTEMUV;jzjm5mCZ^O+xGaZ$FRC?1v5f>pj*Bt|o;@($*Oekv82e`TC^%BzU- z%1{S^;fe2F$3Iut{)gIFC2BXSyph@HFM;t56U>KK8UMOA$7{m7pk1)#PEIju%sgV& z=JfWy>kf-KVN;y=-5#DuORtJD+(bvQj18C53^*5Sr9{&UBgRXmQ1;!Pl)o%H7F@Y= zLlF!3wvq}2_?oQl>Qq9@jNf+L%o$M!hpkH1lp~ZfRgn~P1O4G?SflpAt_H}XY=PKc z-w@Q|OuoloX7@o(|FWrhB5>$<+ErmjnNY2|(^ljQ-})YTz)x0KO%F{At4dI8B_Pom zP8(-JP^2A9senf1hX^yI|5d0z7y+GGZJd=sxqQIw{EgBp%u|t)x9n_=kM?)rB@Edh zUYBW2`hm5{%lZEut)cvvT4kDZahJh@fgxgnfzkb!I1nW7O``%iIxyZCW0+qsIn8Cu zWCiHg)z`(fJA0lo~Nf zdU$$XE*^Kp(ZN?YRWj z7Y#K!t|mZm|B@DaX5&+Fk8yk%VxY?SbL;a?TCI$)K2jOeWTTa_wy#qhU)?Xg#tJiY z2HYlY_>@rmXZTmW>Hl)4$l;{XTK9tt)2EBEgD{PSm@Gs3p?OfzGNZY>4=C@H)F0VGEL^R${1*y zPHxHN5IuretA(sfg?*ion1u0dgiW!FQEDfObXyMhLnouoii60`mJ=OTaGg1J`}z*0 zoXwU>8C}LvN58w^)L?=Ot;?E-ZA?KQEq%yptLNK#YE%wG%8Y;i`;bK{`#F&mMJLI_0 z))s9S@M%9obRTk$=8(RQw+g6Na2>vKvFBVlwK0r+RS32c33jLxySf?; za6@W^QpedwR_ar=oJ)~v_)axra&A55>bmx7$pk7uAV+XLD6lzReBwP9N)MqYu9%RQ z?9-gB&PkMs4RM1Q-}_!w*utsY?r`B6mE6|T%3$055_I$TH(%p|Zf#$QdX;n4!GYtl z1=b-folk&(A5pj;ne*eju+|+qV*EkbR3S)wsiF)TR{~LZXcqHBY={{|kH{(@IfSBQ z!xLCW_u3M+yVnNpCNOo8bj(9^y6=fSqjH?OP|!#JxQFx4ahu3qwj>6!X*9{NFMWs@ z@DQUa7b*!?{)z7|w%i20jD|Wp8FM508}wzjOzTIX*Cf#XB$DEnqJz3^>4> z9Lkx3w@Vb!97qH9cU^bQ;l7IYT|Tr6NJxh&jel1ft0shRk164(>Z7YmwqR%%Mc8DOV}6rdvN7XZsOI1n+RMra zw2R89h}1RXVpoI2WR*sDqjbrgLKcIp^Q9@7dSi$@WV<&gI_6henfBfiXkz}!Ha@f_ zxI&B-ichteLN~>5#y7hH{Dg?OF{sFn2&ZK$FVm|IbRU%2K(9y}O8va|wkL65<;6~4 zYF9J2V5afnySJzT*=PRT{C&_bCTOzuc5U{o(?#P@DAzg$Z=WNfIU{ZgwLYi9ft_(f zDYe7D;2B+w6X?te>0j@Ba3hcJO@f5sCDw*gO>0w-YoWtt8`)5RZXwWn_xRb`{YESv*D4co08sQy=0O<%@SAK#c&6^8slv>oEp=RdFp-K zp+wtuv_h%IL$Pcg;F{#Pl*~Tw1;dJ?%`=m!$?&hO$gmpHIqzi(za)Aya=DfCyGY58 zt3z{oep=Cai@+E+q;$hpZ1a}p!oYg?4xiz`C)pFBZp3eO%3tL*$2R-Nx9E@%w>S4J zf8Ss;R^3KW31<4wsn_52u&`i@y?Ny`gsp3$9pR&fF?v^aK;T(UaVsv>;$(dEih!$9 zjmw0`qi?R575*U;mXM}I&1Q3|xA(U~YVkp@x|;Dh_H#}H(NP8KcEfzMkm>b|M+If_ z;Zbx@lvUoA5q3*l(5m(@hjMAScMmMF{aSoB>A!TyJJN{no?<50R_ZDvKfQJg4*k4# zy2Bm>e?HjU0T0>S9&tUz93btxwr&?3btYbhzdTwz!zj;gO9s#c{phdykwOF%*xb>+ z~?=DSkH5nmZrET=DaH=P<#zZJKd>qI@71qdN}QfRt-eT;_Nr0QopTYm`vbn^Os$= z6E2AefWaK4E2%dDJ_Ro=vS9L$EHT*h2zQ(x|M^LG0`gTN3RFp9!+;b6=s0!)=p3P6 zqBJQUFm-X^xML4X2arl=*Wgdly9kmBm*8McbtbC$Ze^DXQ7 zf;n-kr}tV3kxgtZFfRY5Ass$fVayY(B@B%IWzk22$o5Nb=%}l1u!7VNa~ad*D?cW+ z2Qb@l#w(Y(VxFUsToIG42)X9<2@!zIqD?etn2$=+3CMC&LgjgZ+ruUm02Zd z-HV++{mag~Hoy0)DLs8 zPDknHVX6y8T}#vzl$&1B#LbKgv|lj%HldTRE>3zi`?<)A|2})0{>2pUh#vz+jWN znHd0p;0IyA&K2w8bVz9+bb2dF$=r0Bh40)-DGZ}5eWIdX5>-I~P4f1+W!Cr_^nS0uR_K$~OL3I_col!8Fe&QqC=4ZogN26^eEw{tozrryDssR(J z0dnw~F%P?%V+(h?t*KjXM)AF7Vpdrz6Q{i&&$c1jq6iw)8S zRh1U_Mz$8^d2;l{I-?EoSsjH{^1OjF&4(vyyxOyRQWqgrrw?J-c<}E#da4&=m)i)+ z7ul`$giK2C%}_H8+cPC$v?izJD8Lid^xy^}coqK7^EUWgM_o0?GMnrj$H2en@~}+Z zAyQ2fy3B7X(W+i?a3Q`q3{L((H=1Jy4jx1Hi593W2sRej7>YXWCVu{8Wl*Ngf7;}l z*7qqearU`Jqt@+83`bf-D_Y7rt44O5%AU~{C!U!24j-qbb^MNe#h=M~e+<+QmwI?j zI75K2Hdz`&g-$~pczx2M4vVElg>4^~7sVfb`)%+z>J+1ZTA^1uoJtl_QokFHfgm@q ziQYAOUGL)fQgh&u8?&kO!UP4`IrC5bF`?q=ycGrxAq@pZMF-HqwKZ!8;zt4_&84Ko zbhzwK?6>JV-P^nxL=eI5`2cly!=Y!jo^GA<+HbjQ_3G~IQqJ0Xyad~7G5b4KRt#k# zXb3nv#mSm?#bLJxzIdL8Scv-dnnPUc-Nc)mk0#+^Icp`R$i2$?EwvmUV4vXHtI3xu zg*HDBwTF;FKqxk6cSt(t2VUR&9b7=wzSqKFReSc< z89T#J^2HHu-I9y){M;=F1`1fZ!}}`U_xR8qGQQJ><0c`=T)f1nu@ArYCY1bZ#J($f z;_i*aKhKztgzGcV0Qg!zA^t4n25}<->0eICUd=ug3I-uB9SdU2y2F@q1HksM8uhM8?+yzF^nW+tQp33I}`WyN-W zz9syn=WabD1KzlSBHLEJ?%EqU>@cYVwQ(c1=Y%2USUxk^2@Mmcuig5~6l`I|N?pb6 zXNl_o$`aZlg^N(pLy9JL`@e=z{nKb7tH)p@?;hzHyP{G{y{(*19|HgAbXsK?ybQq8 z^w13C7PJWLQ;|GBc6T*vtui_Z+H*Pq7i+9Yx39nym->+7|+~PtFvMhPFa%bjdoZC76Jm% z&TK@Pk`%b{Gh|r;Fvq-dTm|V4DewKzj|~o|c#I~*LSV1t=aF?8eiiM~!irAhWS;mUSAI@1w^m1^b!2k2`96j#=@c2^|r z99WJ`qChmESZ8bO(|z7*0t3O|3d+xB?a#-M!+o?`qU4p+yWB=={omk*lm_AjXj)L& zRV8oUuL3I}D9A7?wS-muSwkLzUrc$oxiSK-0MRXG#sCwqPhS6|if^HZQf*nXcZwD4 zTngbxk(&;`=esa-Dx3piH9V2EWsOU=)i*j&B<)ZY9E!MXj}hI)KWAfZROB2u5hU<`U~dIe;#{k zKExY3cngzaA8kwn=o>upumY$#T>u2kl=eqwz_mHvC!nX*Vi0KX@H>G4W;o4psF z?0MM2hCxQ1C;0lKxcRf4gS;4*cACaU%BpA_NVJUci}O$?J*5+vk@~nWcXV~jjfqVk zJv@OGP|cEc%$-u-a)(e(9j&^Pb;O%owD=l_Q}%M{%_iEzg`0I>gk*AFBw|X*C9{db zWO7;5nDKC$=YUGB;0bd`F(b+)ur;c?XgwFX^D zv}HE}4%u2nOM^AXu~Hl;j)qel-E?SixO!_kbx?<$(aff<(Bw5WJ}EY4h7=omJ9x_< zqCMT@l`UL%2N->j6*IDyguvp^Lq6Gqsi$TlhZuQnd zJLmAD=7A3HQ6egJk8h7U)kg4u9hK8@Ce0Fo$G1Pc>5zlp%xM=ppp3~@)8$?5Tj5vP z*Q>|^a%?ONNvgSr#ixDTYr;euM25?tR_*40`BC#-OX-89Wv94UH7K%tzuE3Buf_H8 zAhBd&oS+$izJv{Kh15G#o&GK{7!A)@1VeUQh|U_y?Ekysu3c7?Ot>{3fX+I+?_t8T zz%xxmzLa|F!=X49lCabaQ9#gQ4PcUJq=33 z3iMeSJ-%x_VbU>X=P0$ew{_{~2>7l&Ijw1SCMEvhP_w$B_?y&b^>ZXvaHm^1NvKc`*7p7=3QP(`k)Od`_0-kMdP_$0W-*)`)ge0+q%mRrQT$O=gc?~jc^H^48M&D`ijYG>{tgyWC)crkkdiu$*&Sv*N|$P07=kZ zqDu{nwI#OXI6{__jZ75oL}mmG6i<<;Y4eG88loYRl)eXwA2tugToV5wcrh zDD8~tpwB#0;(4_2m`Sp1<#2m%%VO03p_Dvc!$#Gs;gL+iA^n|^*G24nSvhHC%Y2bf zisZbEQ`tH-_j`@oJN9h)h!x@30Xkx#ZjReuFI|!@fI-OAt*lEiX=xBWO$&=Vt6?*! zH!DM%YEi={D_8ZL&_}z($VaDScad1b=Xb8kIof-g9QGo&rcVNq+PP~l9Dbfk1#NV1 z*+SbnTdF5Y?w`OqvO{fKLgH>qA&vSRt~ zZH@-IfNqqniFBRR{b((KhkI=57|0Xy=^{C&^D>9~=kKNUgoO}fLax#gt&!40pGq?#@yJ>_G z8Bv~X_n8!;$qJ+>vQmHAp{+05Npv%QKQih;2O@daj&pLdRyD)a3W0x`)29Xc$9WH* zg=H`rJ3}ul4t#Xzkv-;XWCw`;oJblwlgO3s^xLKP;@!%}j@F@@Q?_(_>=5Hf`)*v?u*g8=3@= zR+i*i!nai4;n?RYzhB67TUGZ%X0Ot(07|0=&|DoO)xrduNhd7lRQ`b@Tzijx|4d;o zRR^E6Jss#g2!a$+CgmrtnZgC@vbes!YY8Qzk+g?Doz;HBzC%&@sdsGks+$VX$`GV? zdT;mfxmqL|wgrjNK4Ni%RoW!YImV;q&WjR_9=<3_{mmmle1Es%!}lwA z0yq*jtsbI#)d)!5RePKL;DQ5YVkqO}ZXfvR`slyE!vEv6$s+a0n7EZK{+qpLzF=}$ zgQt=otBl-!E^gNTG7<-9pXWU?rwZ>?X?!I(N#6hXNlpl?;G#TrVN64{ zwA}yx`I{TV1XX%7@Eu1}h37TO>?2>+Cj6@b3OD|3$6Pna<{{Ex+^^(s>~B%~?6S-h z?@uWgbEAt&^D%9vK4{zP_RvWKY`&J^w@S7{*>MT@B=)^X^K?}ss1wNV5KM;E_Q>DD zMMczu>XFfAW}J7J1xAm7Xu`Dz_+Bn1=4vP}kY}HzjBF?pysHv0$bAJB>iWs%V}ih0 zM-q;knEJ`h+5y#q+i*CHTE1+}&dTT;IdcTY-;i&6_OW!VI6hx8!Lj{ABFT>?P)D(R zyI*&4-RuPZfq)}qZL}b3`cHr(mDEujJJuRg9GpHvqTmnOvH&6Az|S5f^~lpztPSZT z?NEzrjBKF2AetUQq1~{YZ7+xGsP+**ba}7zpMe0CIQP;#ld)(=)B-<5sVF1F;bctX zx@$bS4hORuT=;OiX`qfr<0}Mw7I7>8+nTn;ni+;g<-%Yh%fw(lg#uGD1>0}$&aVumVRuP@rvu$ z_!=q;$AlR`q?S$c?bTjddwaYFq0T22L8$7NC0p}jq9q0kxPS8x&R`nW#xj)Pbrl=) zjU!l{rbYrbPSDF71;$Knjvon|wf8Q~RO%0Td&2)G$Y;nZbh6gz4=t~F}=OoyZ9d#!<4p!T6LoS=7ym+!T+AAKGs(aCfdz*rc$N)5NvbU1PZPO$nR295`{Bjiz)3a zzc|WrD^~nUQP1}IqhGLw)$VFYbXve~y<&awz~g4<#=NCWt!d%g*kzOT$%S{KDm8sk zn#}Euah}y{8XoQS)U&7BNo%}h#=hJbBvk}#L$=PABsSyDt%0N4a-?S2P`%~T2s|ig-UKEm0MC#kbqBJTbCNKGuaV;46M}n`*2cGMlu2?^YS!pWA%{I*2c-} zl2|j?m|+Su9TjuEHx&D(;DEtmeHbPFU=r5tPP<1A@Qx;UZ+S>AK*!Q6 z5ygj^7q}c(qdp9NPqwI5Qc_n317>gmCoU?f9RUf-m=D6E_mVKvSf%`lJ1TJVK#wwy>0;L z#iOxk$4glzfE#ER$FMuI?3d0Ip#M4Y))!kKr^x_F=TvUtq25O-V?2mXH;n;(Qc837 zoYN0K-imnbZMMkITOpqUODgSy3e|K{EGVhW9UIy%*V&$QqoV4v|sgytHhdhurkA-CG7BY^>e-qU_1I!L(V|rGHSn-`vrn1z&BkD^y;# zw5P>Q0M&KK{?t|tVnM)_w*aasGYtx(w7wl_$-3GQ-j-FpV z&8dvn++zg|L$j2bU84bBT$MwP zN$@Yd7G^?}CS1y<#Cwr8);11Mu=Wra`?dTq`Qt(-E7k2KZr_JOjMN)--+UI!M^S2&#`2 z2xw0*n~=3hSwu-zUnxFm;;HP!a{sacn($23g&nEJt4qM1Gc80U%QbCWug~8h|6U4} ztuN=^Rq1@~SbQVgeJQK_`4$_BJe1BY6@V(Bl07uO<}D$=KLg}3js18@1;gN@$8+Bq z!PB25fLNkXlCK+Hq4v$0M@kI0H`YEEIJNMSojyHa|R2|1G~Q6bmsgdRFwmJCks^|%K~2nGi7Axn75i@xm3)k5Ms;M z*5AZ4@xkx^$~!hbOIHG8{Qt}udpj(o7NB3h3_yPU;`mQ{`LrAZpt15y?VzH2O}c<@ z@To!cZCMF2LIJX6c3*ghd@N2z$9=%0@U<2dR*2vYWd0CUfB9 z?el=b&&Ou6FbsptLxW{o+F0+O$3dac?S@qxK;5TbsE}e>w5s7%g6#gY$fb<6Z=%zx z?q5pX_NWWRwZ)tqz{ERWw3os4L-cU#&46$wBYZLHfv-&Ehydzo{qosz{>C@C-{Y02K=iS_YmrqVtQu znQs~D{kt}PNrNg}g8S~oOuofQDBny?Go1}i^$QFCI~`c4(7$^Y5_sH{WKPW^(PPrh zzmOic&AV1)gG9jvhGHEnAMq+?SI>F7uOQpd3swG{=^S-JLg843b=W8zp~{?N)GK7E zK4;EQL;cP~svrBowj*K=4q6>x$&3jWkr*S2W@C&YrfS+X zbSPGVP4F%@MeDUbZO8d#JZ%(DWY3})v2Zw3s<;#%Dh0}<2H`bbiy{S(&uM!jZg(@< zwHlcX1h1Q(()Vjlch8q8{_lrj{$E)`J0!SHbYaH4z$hyuNp_=gsfNPAWE)_bsHy-S zJV8*-wR%zN;Js0u7=a<#wH~s8l89=^m^~CEZ>6uugLFndw7$~2bVwI(wIXv>Z@J?c zaR+4mxV@H$6BQnUVGNS6J!wO4&7@x90rjET6_K}&2>YNrS)^XHVHiVi?tq)!&VX+t z%pI76cc)iTGzKaTE?tdWLXadWJ?>HdjL9lg+jUE!J~!e~5*L z*`(09A&dR2$f@80b2bcg#zCMoG%!jq?b3Rw>_i%seHHfePY&icsQxI!SqqglfMvHT z(`1WZx6YXgf!cLqIZ|{$PIo!`iOH*3P&QLQ{NOzwteV%H+1})W$-bm@Wiqi= zi5>uOIFeSMEC^V8)oy&D|FDVkY_>UJI4gFQiprM9}%Hk-e_N65;DDM1~On`4H3NMpB6JDP-9i z9o;W$Y_-5tm4Nf?cO)il=#s>0e5xLRF#z!0L78w+igZ2`79!l!ZF*=f*j_5RBc2c# zLO>OaDF3I}8d@;$UjsUn6d$jm+tL;0|NEU3_NuA_4lhe+z8j zV1rS7%hTMii>&+HFOMEg?&T1yPxQ|tcDbR4AxH_sBu8p)<+mGroVPJToBA{<@LXNF z3@yO1Bw8%4TyVo&xb3B|3arej@!gZ=vay@jhL3@7o&luGyE-;RV@DRE9g9!iRSkG_ zmmi8jp1T_G@VXj$om!=0>H<cMZA*6gHmhBHx6Q%4gGaJBu;6WgUlDfG;L(C`TLfU zP4qW0IPw^`MTIt}kk+odsvoQN?2Q)JwdH$?2(p%t5pZJ9)Hkx^kvD)lzACRhLV%n} zMbv?uDXWUug|808Rr3p4eXb#J)CsLx#}chcG}hr1-k~h7J0j+xPj{>E-Q{P|wJh_c zYzj1<2){OPFN>JI%HZaObc|X^7HlH%M~ONI4XFz^TxpiZKg+OgWg5DzQ@e$wXU34_ zaZS`Z!AwD^dwt6?Rq#gWGKJ=%>gZi^9WL&> zO492?=x?6Z)=1wPWL`LI`}ZinZ9XYe1n!0Kz{xrRVpJTEd~$dw@i?fPSgA$?kX^Z_ zD*51TQjguj9C2)#KY=Ij%pENar~BX&_!d4LGWCvnt&W<(J@$NNJp!Zc*p6CUjWrlE z{l+{(Oj1qeki9Q@ud010O42iD_UZ`m5B1U)V{Fg1xvt*r-nh0!l2cr16i;uqEHJ_R z)J&D0Hk0k3@Lf0ZP_h5PEPZDdPRQ_w@c|`R$3KVR zQSJM5eLQ%?d}NaNX7ySX%q@7#&#BJA4#ejPM>7JQ3ohN1n)hfAl5U(R1{?21Qq70K z^X+_f(aXbv+B9M2(h%Gy3qq+awB*K;?Wlxr$C=CT#H=wg(QY_NRb?Ggc5<@5@aat5 zpUi{^`ypXbNbF0NSOtp~-L!8dvh631E+dQ5i+8;C?xCNtmFSEo-H_L!Zp?oFFW^lO zVCtg(2bkpjQ-aR;pYTO7iwB5S+fv3+Mg7)Is3W4Kn+1lOM~|f2W2uf%QL0M;55Ff9 zqQF40f zp!pxaI(ZXu`Pka|y2TK4A$1BJEM~X~!<4eIV8w8^7?P6!!yTlgyRt55F3xl6<~;`( zVo#^}Q7kr6?&7toSps-@Ow-5m!Ig!=Pym{gnwr{ z#h9rtKL!ae=F61Rr{{#+&x4*vhS8~!uT{{p#jUkAF8f?};PI@Wv}?c?F}B+3p+e)>^VJ6ZURFMmeom1fMhA~Y~|77_D@m##aSPkLYPnMef1Hj2<=~PH{pA&e@ zKOXR8WfoP&p8|PtIP?YJi@VPfGqThLs`+!b$rQ^P4B|W37wVXzSOd$-i^vgqIh&dF=#R*jcfgpX8;=}qSf<^2-&=8_xs>U@OG|w_YFT@oh1EQj|=T|YU_Ps8r z*W#)eJkq61d5|lZQ8f6$$5n4VK2b9#drQ6RTDrBWFD(~K)!i$Z_JB%o6N9wAG@*{Y zHz50F%%W-L$K-$DCWfniJn6vcL=rf0g;dJl|5OP_hDdDKV=g~`k>A!P^na`4zF829 z`2?ZARo!y#J@jJ;Q1se{+`PHJ8APxH8^SWf!f3<7vy;Vhmt^4I|!)B zGt98)AP&|nk}-r|AP?Yxs1u5FiY3-MNRIAR0hh)v@a@J&OAm^%@9%tPi;1z1c6nWB z=lq8H2!qNyDVKLF$B~ce8V-dz*F8Iovg(LNN**XfEqL9}izXohPE|O32_%Fdj3ZAi z$ckkm2IZs=S1?BCTvq=0YYaM$ifl9wmbn&`s$3A8QT(F}0qrM< z<0cXrFacfwC?{CoIduOH4>Xv;ZD5gx{o-t3K_O|1R@3&Eg_~`{h^jfI&EExFMAJ{?%aFh!4Z~QxS!~)zv?qxG?7w^JuSLdP2Q>KMFGjA6f z5KS*3pZxLkAV9b|i6q$FlPvLN3_`g3K+W||Q^Mbm`gl;bH?OH z+W=(-+&$xmwNw%Z$bouljDeb9>bFmbdP%c&z1*A}vs+B8t6Mw2nOSF95-?BYUEpBh zr6FH_NCs9{SajUmIZbpV+&$X;A95_2t<6Ep@ZJ-2@q?UYJMpJAz#1n+OkyG%_Upu>6}{)aJ&qL<%M2-?95l;`<&&y zd4sNhN8`R=aODPUPIw&`izYAAnCK0KV=bdwW3{!o3R`mZe8g^ zo5m$glSEU@9yj<6TQxfORA+m@{=1vT$}~& z&HAL0g;WbDMZxZwk4HtMSLT7|aRCj?E=0ywXF(KnLSiY%nk(dyZgF#4YdU>yG42zu zAyKI&T{71ME4H;>ML5|>V0uD4Z{H^2RWJx~RN{r=_Vo1pMRxQpGQ+9Mh8gTP_nHkl!jwv2w!yL{px7YaRY9%S)`dK@qqVD0|ncv z$nC04aDE@vR-@84R-F6{L*sD7zxN66A3JJ zs#?l=*@}wMtS^bnfQ1q5@*}21ux>?@qT-X#0ApFtuCm4R-g=`o(J%*!@h> z_1cc~XFB{46>prK{9)Y&Pe5K=bF^f)h841wZZBJNi;uS;Yl?>(FyMxTLZh_WhzF&H?fVk8f*D5`+KObjJxmU?C%J!2D_uwlnjbaj(7muo{rTXm@m|z&Jo`0yaye#+U zL+NB8al)H}!!W%x>!osV<3>*&Pr>=UFJLkmF{|+R16Sz;jYnj& zK+y-CB=i=S(IKv*)OM#MC48H-BYXWu>yA-TFoqQrd3wg|Kd`i!8%Q5+6WdY{bbc(U z9fv2;=c2?+sty4|*!7aKz@cOUvkwa=vV>&C9R!eL$P#AqjYW?O^F$jNq+U8c88@2l`HI1hjB{#uw3KAwa0v;;-JOc<0J&4RoeO?@Xh<*gO1; zFW40~@4IT#&du8Ig`SB<{Yb`EYpu|B*3 zoSGQ2T7m4Lk4jovHTpuWQ4IkWM3N|ujM?M(rSpt<)Gj3y*+&B|q2l*5AwRhi<3pXOS|fFagfAqX zU~@!Qyjg!yRy$(r<=O9{DGj0TbevNJQ_u~{l8taDgdrb@>k&B_BMkf@yN=#e#OGa} z>meA_?;r<5zh`Mj$k1Tv(z74zu-c`BWEF>S1t3T|wcwl|R7tikQITw+S1qH^WxSRr z`bP*cR$AB*oecdMEv#PQw5K$u&$k1&b!muqG6%m}xKolCAZE@EY9si7nv=Oli4hrg zdV=1k=kfcUpjRaeIbUg!GIsrYj$WXYWYDLoYz$-{mKb#Jwgk(j2c8Uln>CUy^u*z% z4xnL|J=8Kjc}|A*rXUWT#BAMM8IY;zik}V*IBjFjB`4NyaDv|m9RqoJ9M(3k3-sk? z5I8$%mj!J~F>A<1bDoH?* zz$lx@U~=+ExT7g;5QlqAIM-5ggH&q~~mFiBOSYV(wi(ttFH+rh)5jnuI!TFypTSKcV!TRJ{yy4 z%a{Yjn?P6Si)sv~8_+ps(|NH73R+IKW{8k<{yt@I*!#8e72Tq@mpa0WZ%2JTe|S#3 zM;GwD_YM3%e+?E*BVh=BAizK5-(MuZ5{c%>68~D_LLLGC_b>rgs-IKlPKG8nrgZL3 zh7JyUr1FFL0^uc)R?*6HIOOk)uaMwj5{HG=I+TJ% zGp6(BCs%P5UZxKXCa6h?$y}(XblQIpwM!rF_EsqW?djLeaEt$+QYhN^7wVf~O$|`C_xR^JkwPdGU z+3)!UZORctR47`sAF(NPu4EFtpt=bP>=Out;uA%4nRNAnx~FhM=o^uq^2vj}*l+Qr zYqZ$mdG1=~m1#5sEPQvcUFkE`wmCG`j38S(T{B+(F_-t^ST0@HCA)N*<}8}T76Rl- zH+mZPB)EH61p(M0ef-Rr44&8w$jN!>Rw({wxqp3&f)NT?!NFLfm~K1JfZKv5{7CP5 z2#>?pdB)5WJn_`6#H2~DO8;5W-op04eY=2tU51DxCRG#Gq1F<%p;9Q-y<3Zrs1~&acWel8U-3a4iQi)xcSqh3Fv-RV;8hT@KN`2b&mT7Wfrj9##cI5oBwdDu9{ zZXH)-+(zx0-lKa%IU|vFy>Xs)-QPr1jQ<++i(2cAi`z5qHGV}{@1Xjn7_i5?H>>^m z;Np>>h5`1g*P(zJFOq-60Bn!qrP$ueK(kl0cPr@1Mw zdQ*#jNN;u-JpyvULBjv7qU*J!>z0t-UsZGBC=sFOAI1k3eQMi`30L}N z(L`w0L$-5IWADb7-0=&*_Y3Ur#4CA}EeFMcHzrV)wJ1S~mLrfo%vk~EcK9wLy(r)o znm$r6xgJ*#8w)EV%6-6sVQU=PQdGhVQoTQ`HX<0Qzk*{dybo1aZ?lISTv|*pgietC zp~dbP8kwu4rfg+NWo|ioG0QAg$|8HAk#mV&DE5A&w zrLE%V@}IV+-umGJ_U}a@f9lN2mtg?%z8PsD+I-422MLiD3%PlgM{N zismH4`Ex{2zSXx3P3E|k)$pv6rLcT-W@V)nJxlRPljca+fjvjz5glFix|W#GL|V?m z)c~@QW9~mm?Z!n@VVo=dI7HmvEEy7LhGr3!6B%p_(?J7fT5RYl(iqn6jY9yJ0jPBv zE|#u~H96Jekw&`w&iP*p7ue+|)`90uQ)^al=S>;zHRF`yZS+L#hM@8O4r-0&D^!hC z+xo&8-AjMqvcjv%f{p35NnuB<%jSFIZSX4EDNdX6EC1}{C@FjUG9S7P^Y4|OA3u4} zjIA441{d^=dRfcGVz`nNY8C(pt@vt>m>0D2^UO3SRJ!u-m8y{xV^WZT-#uLMin8I= zJvfviJL+LYu`ZYf80}wCW>Ig%O~Twx1})P zSZ!H!MR*b8k=%XzAI*AA(`s2>713)|bc$Ik~<=bOrbPJ8b9LG=*C<*Ns-NwCHI@CsxVH9*0K0FN|Qe3~FzwZX2C9 zSz$YfDJi@5{?uFPt=#f_x3%6~Sc_jg$;HDoP$6X52QXB^Rf@c}2LG#OiG$wX9S@z_4HdKsoWXBROh9+s@! z7##Aqb{Vm9p|9YPv9uHGk}#0I)M}KCfLL5U4yX7ngzz&OKb@cg=q{$+ZtGZzv8DBf z-iUSifvz7gHk}V)Oq`1&P2BZD1~mW759$cB4%Ex1gp3HF z2)k0yo(D}h-_!B~OW*PYu=))Q|Q? zOLPcFPMrKh0?_6#)y`89>>^PMYMF&0CJaMQibRDLl)T+(sB+D}Ot>QM37FY~F(?ou zWBOvbQ}hNm&T7=o(=dP`x|`v2HaqrqUQ1tlc$itS|23bMI_oEbM<)ptEg>O6geSmo z?fB@piID&Vg&T;Az!5?Q%1A8OPZBeNixr}E(X1Brqk-2OL(=6BWj%}Y$ddw6Fj zP^PgXbTpEFdMl-6avf6ljE<9wGY&E|o6y-)rQ=}xsC3n*>H~CWD~gtG&W#T#aU)pr za4qoT0U6^GnCEaleF?HOt%jB%(@ev&hqEzM$XY>94J@71z40hunllvWw8{$)!pT|m z?lUxXV-U4A$D;exx2F?WEqt7+=vp;{+-DoyT*Lwm1yxg{IM1cK9{qK0^s2k~*5fs( zGaE%3N=A|_A{Ff;gmjphB?U3o1>RcJE?y!vfB?t?Aw(ipM;nn4!rOn~G3lF1a{9p9O`hPW8EP(OpW#?6HUf^vHOBQdLWPwCK5`rn8b_G~`89 zK*0b!Q(5*y#n7xGACuEWWrfkz7M;1{Uw9P*%W7!@xR&f7SGbkj6X{SpIigG zobCPcN(|9|zmq#&Efwa(dOyUxaO)H#U|9>gZo~e8zuC@PCQR$C2hOchcofvX!gmpz zUJ9~Q!wC(OD-4h$87Ny2>A{Ri5TrVQ{mePZkQp>?lUv4Je7PME|3m(UlrwWX)SEXP zcvF^r_2qv{|Ed&4`bD`Rxb;k5M3&zg`B#ZnuFBvgr_Oi7O`{DN-OmPG)&_lwHWV_m zbt{}JBSTv(6-sSmIJjIf5_@;5PVGm-zArW1#_lK;RJY+WGiq7gJD zyPjFZP3Hb7v4feiA~!@p1d-iI+s5!|pXs{zm<A# zVn&hsUsni3+xApZZ#-!wycdaI|Lfnn7)zwE;Kz0&Pm}}j0sfEE_Wg&h+lj8JjF3+q z)St$|FM@bj4Ux}PK0c35Meizd0KDAX+8nOIOB49 za{9~6%-!z&VWpMemzm>+UyLG%Wt3|oYfYgAVYnoSa-ECJMVjHLN|#r5q}3P_`+&k& zB3mW7=TdVuAmTzpzTIYZsn{nMEMyT+oa0M3B);C`<&Ig{X{-{NrxccE<4IPV?;w+2 zQ!c3s+I>QYO9~-c5-?%OXmZp2X#4Ll`o=@3d_ri|Y3wLEM7F|}(TUV7E(kZ~y0q%S z^~-lb@2UMUQ!M1GexBwlMVlUj&3Y*{ri?Dio{_W-P*r}oj*jKUgCuyGW_oHpK2_Fq zstkvNH;QL8gfTa)c5)N^&zz@zKb(Kb?Vu=vxEK;}$R1!E*^gH>CO=xc2f!6C#*O z56sY80gN+@o>kx`X&lpQER*=XY^M*={Hh^yEjYZFJX7|=BVLd-)=trHBGq_@LfhSZ+-C32~_~Ote@ghrBbD0*1DOz7aqf`~R zZq{dFLGIkb$m#(DoY8pOyt5b{Ibi>yx+vdLz$}5#iG`Y;*1mCMGBM6-B&4u46Kg{j zJZ2yV1~dKwd_>HqJLyW~u{o(qh;A~d`N{AitEN%0#4Pn-eR$88G4Ub!^da85EfB04QD&KD4k}o%k&+@BOn7E& zZYVds;!I$f4vOqyzI!g=hqa=&7Z%6(;{f2pg+~YrBj92C|Ct&p8X4II`bpJf!X!?| zQ6yRi-~ld_HpEcBmeH$7A_v>jf?A5;*?_$JHWLgoNx?F-9UZhNn#jAygdEXqI7udC z_3~q9TP4ibiKrHezPT1!Pj`BRxp`?g4U7V1$XPxMw|L*rvh4Y@dAR^z?-ww?oIMDQ zAtNbHR1*Yn;4E(I;?efC10-uvjY`H4qMg2PIM3uOh?0mO1X3 z`!!A|9X1V5TYFis>#;)Wy|*fgXi_>nE8$Wz*A3#-F0{D@s0=mim&ZF?)#=p7kf&GJnmh8fLNr>V67nVxHRlKx z=>VS{hHRHcjhpvld7I3#TUyl>(IIkmqVfs#H8H0}g0=+fqFK|8jIfJS1=U(^d-1l5 zvN1E5;9KoDk?gkj&7A3gGT+*g$_h!>y6eW@#&ExN@knhk;u=RS|#&MBPCB=%i;P?tLEWYaROl3 z9!XS4wUuT1&4u3$53&kd@N2>xg?5-ZdshmGYKkmk(tF}tUd4gHx$hwQmTTyEH}22E z5dA*9H(r`YK<2VBzJ6`ZrcZb8z)$C(4>PPTjaa-WPEeIyz=>Ynht;k5hkyN$gi~pW zv&4!9qKFBPSyrLylDX&2E&E$puN$yow{9MX1Uvl4ux?Dy3ml>BUHG@^w$Pa+!)mt` zwpTV(va{4whE#(|$88s&N$F29_xzDA(#_3p$^raus2hU*Z!)`k4n3seYic1`ESO*s z&(d@PKEB#fb1xCzT77E9;&@-^JRgyfnKj$$X*pLknMizt1+?^v7 z!EBq56})1rcM2^N_6XUWta*H1iKkc_G#FvWnQE9GXXHjuZp5qk)*?M31fwhFLCGz=6x(?a!;<}= z(7`%Ro|{g+=C3@)O>OjFbr+tBXY&`6k&;{kw!-CkhUc9w1aP5NP<&@(@vQra(3Iz*gleCyT#~- zw3qt)fQUE31sra=Ge(b92=Of0Kbvj|*Nhp|3v z9f%7DsN$V+Tp6Qr@b|YFvacJfd^OTjkzWuW-m_pYRBl>Ul%@OD1nJJ}TI< z-pnP%*wge0H2tc*b#w|Z)8{#@F+Hu^2JsQilS|aUr!Sv?$(pWQMsvyzo5it|x#oU) z9}8D!!1XxYMwXoGXn)@L8bEUO?RUeB8`+#JKU#f%1N!fvH;j_}O3DD@SScYCw|Wt% zW7R($)Pda^zLjz21&em@bweZS08c0Uq;>V0+C6p#x_@n0t!zaALUQ?%64uhFYOX^R|g3`kEUOI zZ(dw`XFA`PRsf$kesVIQv!KmUDW60^NQsCDM8Be$0I_!oyQX0n*{L8h00klDFheLY z$ku*X!C^?@SO~-9fsg@_azt##@eL-ZP$7+Aia-zQAaowQein}u`5YL--@fBP{4o0` zQJy`x2)h_vU4(n0d|zY)-xavM!w<-MM$4&WUBr8;Kx?>;E(@pbv|ADq9@&iC=7ts< z)|eSffJ7Br?MAS5l9FT#c?~O`^5&5QuRpPzJXP3tVRjasDih099cv#y)mvWJpmy!> zl_SE7NAYtGN(A3FtcX23KT`ES_p2RgbCYp&wU8ODQhz z8vdMZ==Av8Q&2t^RV3)j6!-S!RboZF#mmhWU}!&!XshO;?Q4n`Ce5qS?o=-IZ)$Y# z7zS2we-6Xt=@4h5r2yfq@$i`TL>g%Z_I9mi=hCNSzlzvoS_ZBHUG@tf^WSacvTegO z;}G+uEDJko8v$Rnd)ragj%pDr4HjJgKRC_t_W`@QOVCPwrgO*DG_PR)o6=Fa5pBQ)Rh+kssv^G#r8|1v zh<(oenm=!7v|(o-kKJBq7zr0`b<>t|{Wi>*a-bxtyoK2)z0gZ1k|s7i1-UZ>aLx`j zV$v0P!0bj$d;nU&EJsYe=MLP8;o_NVw~?f(cRZW?*`k7KLz|AI0xP+VQ3&DPHO|8P<}_G&PS-`$79fdNAYt9_wHiv(nuk@0f%4 z#q>qIhXPcrfHjsjnDiHKryc+eaG|fOIbo!m3AEN#2}HEelyQZr($>%}Ov$cp&=YS| z8ELYR!l<$G59zm^y&+o6vZO24K&V`-{C$+p^@rpECIVbB$51KR-l+bz$c!|LNL`&_ zDzv>8S*Y^vmFKz%I|C<~RjwmZ(wUY;(c(j@qdWNGrNi=QOWEKCI4hnT5QB04*NF{% z!KTyha~S@7m=|U_8SBiGhx1#B6H5qz*GY>@9)IlMwO^UWyiVV`Yrb$~IoBW?H3I1S zN^@;B>HV#0YLI$M$83nU%-pHX3*MQlRnm{OX#L}vFgfJ?|6G!Ze);IZ^ZqwZ^2#SA z9eb_NIfE{jq+8!Ogv|M8W`~jGz_CTi{CO=#OH(g+-fV3)<7Wo7C|N8=0JZ0MyFRwd zb*fqRdod@Ze>ulMT<5Tu=TpCFXSY6_n|Dizth5Vn~-EdyX_dZQ)6 z&VM1!&e0ms)6OB7*uGYce)cSn?4K0RcUP zeKX*(`eldO&+ll@SD@Ht>F1mch5UVk;Gl zyJA+PV>l1My*Ia9-J8H|PxS-y(-*y}WU?ay57|v$G(8C7D|WpyPItEintb~pbfTb6 z0yoaVS=w4Qufzm<|D!9e`zWJss&IldJ1SSvA^BWlH0a2?TrWa;x z+tF89r6Iw(PHK`;d~_ii(WcnW2FM4*IESwYDo7PY*^kI-0swyyOmXPJ1oixv0rua; zlB9z@1;sx??J;pGP62?1MI9C^BeDk402fbG1~nauNs(c|*r$%MJehHgZqHqC7j6Hz z@G68HJc!}@i$CdZvt<%U8hj$*I&0&nt)c!Zx3||9ByA`m2GofwVU$9Wn$lHE9Qyat zT2w-WW70vI>1-C=jFSj%D`trP>%BC+u5yjnCJRxiWYR$XffPPu0vhdn#1 zhBI?h?_gfZ%LDUaTPx{$)Oo^{ZVobTq5(;*d6qk}CPzi8V~pP}tw@rgO)q3U6#~Qq$L@ovl;BJYj2yNuqi5*+Rva3+bJ0gXn7iRY zGw@z6JC;6LX=+;0E3@dZFW$z1(on?&J7IY_{7!NWdBbk8xO#}nn1;y8NjoP$&t{YP z9|IvDk9VTGSh|WtRGY9wi4x5?ESHFuCh|GOkW~5gL^=^BOy-TOwEBZ+3v-Z9cYtz*`r3KWhRUW`=-e08yrRp7O!PuxVWEI@ z7;^M9rQs$TBFf#ap}|`m8GyB_WQJAKp%emT<~gWp&auGh8aTyfhw+tT;xRO_Id7}x z<}{*1B)!Mn_(QYNhBkf#JWqwkk+Vy5$D%5~C5OO~7;i#-NDB3>aR5d?A5>1oZ} zW}&7ri9HNASn%GCAYpgtEbkEYe|BBv>VF^9S|!~S(#7E1o_46ODi3&Wc%Mkw=8Mp) z)bTB{b3f#D<4H&zHA8C$$8#p>n7BUG8fIY%&uyp)F$zU2g?A^mzQcgz>}g3%?M*ii)MfV7IX3%Ocz z*hTura=%pLnh(0=>*jz|KB5D^_R;{I5?Wr%+kM3))zD!;S!G}=f=Rb?c82YwEgk5_;>6;n#CV>-vo% zH>WBa7N*3!$s!5j8I;$1J`iO|nI2ccsClG#V7<6?GQlvj=`#Row{DkPOj2Jb>@CG- z?J4cZdImMFK#Ec0a38D4XKMA?98{`uA)RIJx0|Hx&}OPSY@%s{8|2Ml_r><*#H;r7oa)0} z#V97Tgf0DI>-jR(-pRLcq6KjnbOtcXZZqUOk{=bUC=NZj(DGAb=~2J`oMsgd=c#0; za4OJ`TKd-lTPo$3u~v{4x=ewJztZil*g_r;aB+?;l5vIQ}5w&ee^H!tZdERQZU?`!s+ue zQqI=|O=Wtd7>V-K!e?Q>q4lcybW!>w047#AJm-vVl!O=5WgGF|kGspa=7m^84r8xi zx!)njv{bu#B}Kw8f(9XTDd~NiYF8{eAKg!K`qgcef4PPrg zr!sEU?7CAl%rCr>;FqmBI`r<%!0YUY+%%nb<~tOl!#DgY=g zj-Ja*`z|{247b7JVZU}d&715ssR;rz4%JTifa4BA(NCfuok&=2vBpihCzg>rgMm7W zEKe-3zEKcRMYagp-l0x*}qBk)S(joK+3t*-A;}t#KkGCl31GTIhOPm!t<7WsD*${ zQoGp=`z|o6;){@#09zPzYVMK%p-mzOX{&szH_n+|vPL*|TG}%FH_Rgd+;^SbbaL|^ zQ|;$nvP90rh*F^nZ(j+qt~~|S)B$oG#cfCb$pRE9s77f8<#9F|l{ucHZ==P-s(k{> zR*9oHKM`M!+`HytsU;xlhl1bK55IOye~sMoi>QF)#7!A|5sZP(dhPm_V!WRm^9>87 zuL>|0W$a}@3;;zTxgkb5WT1#vjgGAf1zRdF2TXE>NQ zC*K0z&Y(&(U-KNT$|j?xOjK3M+#nmXS*>VjJ*z!bedZW{Yno;{I;in}n}#btXQ=c; zX=xhLYtcLX9A*78+(Hp8DbBEg>=E6^{=QcYFxQF?1Pgam1v#R`m5k?~v|fS+mX7u6brmfOC3=y0V;n6vUkkI6j-z1v`{ILexq$I@SE3eC zd)#@C(HNX)^|TsTv19m6_nyeXf}uQBYhD;@4aQyd$y%Gq&vYW~6FsyYyVyrJ1QCTMrwVGPYf0y4Ih&`;tc4pyVBD5QJ7?w zSJcdNQ(?CHfXP_QNoyOF8zr;y+6q<&dQ6OB?8vv&+>zKuF%)T-1=Hx8S8AX^%A52u z9I8hTb_DV!0K!m~RPU|=g^H!(tYuL|AreFSX0AC9`tW;TE%6i=QUcn0=4D()=&8Uz=T3N)S^~eHD-L7ysxWmk!P17WdJb43|1S zQNA#`^htu=Bl)Tvd^wYeNOYN>Fm{t|&8CHb`)%*rk20iqb|avZ+L3@#rYcP#WnAWJ zM|P5jaztL|P&MWT{R5 zTETuccUuiHgU8IH(|J=drD94;5}br0g`P;IF85yQ8&{INww~MY*OdF}uHlRi25oQh zBc?wBfUF(MRWw+Yms6g?`x$o~cdq80KHfi3yT~+LzL`jbF<~Qmg1lL6YjC1vKc0=qf!_Vd=XBu|BYrLLi%nrsKp0ZGA zx32hQUL@Pe%CL?zF8Yn>+6(c+?+6m}u8f`A;UqOMjT+Za#mnFNJ516L2f1LqDb{?K zPtz_SMkne{;nkVT*nc=$>2 z++In(SymIoh|yH+a}+pB9^m&5(;#PXw}4Yej6!WXs41#T%Iq{S8u|gBp4Vx|t&a-Y z7d!#Hn}xF-e4^d(x;w>J15K0|JB@8uoj%EFwt9LF`3EEgP%>D1jMXdyO~fHJ<`EgV zYs4P=jyq7%1ySmD3Imh@rZ_X5*XCM3CgEL*v?Liq6Heydr5^uKoT7AOXv8IYI~i)X zVp`3vmFr#-WbAKH2FDaqWEoEeWFXH-Z3hELA`PNcN~i6@&Ftb6g4r1bGXSsp!i2^0 z1Zva;!ty%;iaSEeZN`4!RhBNR9u^$qqP1lS0>9Fty?=z5!#)KEM3ChHZRzsg#ta_S zzsl|+Q6wAXl)Dz%ZH`4F?m|-(4^H8iHxZp_4OvAUW?UnULvN;x-eQ^`BMbB1E!z!H zppKEh#e98I;pG)IfEzccP1z>O#i{!QX&dWzaA^Hg9HFTmn5_RX6Qtlt7{Nv>5Qt{;PAmn5f)1`19?!(Nt*iazo~U%e@;+Q#DL`3u|r? zKdB9Uru(3Og8ih~X=cn=ClN{ibRgMzopqkM`uu!-jqMNd$<|c4K0%BzZjkHP7m*I2X^rhxQ0#jm%zhSV@!^NYS)m z)48Y%qoGnqofo>OFJc=XUX8jjE16hS^bNUZ<(>1c8?m7}74lQK-l%zoDW!)qLwq2| zB=n7LdxN%s-_Cw1&C?NYQbJ6t7|TD7F1i8FOoE#?ptFY%TZ1-)kr6_bmwB)0k~3z- zR&urta5M*nBa+641+<|&dTj{Ep3}zD4&n3G)yOVcG3LI^HlXFh34j3s6& zu!fKR!!k;-5p*`_iAb8b=`HHRK4IT)@WyrI>J#3er7yLHaH61B?I_6kU}t1fueGWz zMw)a~)33_V76jK7(w-~A=hSl+n~Y<8Q1Pn!;8h1YUdBS~aRti-|7dA0@suAXUW~RbO$z*upu4H^ecy7D3LuM^E#bVK5B~qp} zm(A72$)k=Ed#ZClr!TY-Tq9>+{QOjCEtgJA{cvsCmmujFmEwXXynfDCpHHPH!?#1< zJOap%QV`vfA;2FXR=L(FWm85aw2ge09iy7_>ETlnMs>~YhG~-v^|iQc3nKBWf+k^u z3w1H17=(e$rW1*7tc}Ob#rlN>w@IVl#-R8B}cpF*tgBhJ~Tu~J$4 zyj57vNaTEI?*RbE(=CLxn1W7h6~IVRGBAv3uSQlk@KQNCF52 z56zraC9${pS`w!6@>B;=UgxLIvc~BuPi18PI4B`yna5ZKn_DMIu@yIcnCHTlF`{st zF-G9>h6E?9Q)+76Yj%>?!CblH^!kWc8k^Kd;tREUD2po^2rc>%GH&ROgYpWMMnFgj zb0ygln`-j|>Z1}{K`V?|T`P>mfEyssZZ;s0VwpnijZZM0kO+FtG-XGwrcXaD->4ew zR2;j1=XPL^p`&!ap(8tKWOjdZu1ku51?|@2XFk-}aC@O@13^R&s0kC2H~Zj)>C0lf zdP@y-fPq4P^x&mXzt$ zRmvK5AK`ZSv8u=;IhkrEXv^O^3xmQJbu`+1@Rqaz+4ZJ-a8k|ry?(y~?-Xw+eZT~3 z^gZ7-T?oe|9S5;FzBu%E(#TKfK%)K5QQXX8CT3Rc%$#wAs<6-hItSNOCu0r^4Ghc% zl-EQB3gom0&c_pjQhjWUot?9^t&H^g6P{(#)40978qAjK1dVLAAWl+P8*27cg}`uS zxl(EqkjJ#V5+#Tx!_!b3cto3eP-9VIjnKgZjKUxTn@!;ZmgSpz(1yOEEsfV1F9!+w zBa+&H4G#MK*vOb3JH(B6QgT;xS9eu+onM?+tS0onMe)}LT&WT9UDm#T*5vG5ti{T) zybnTR7Mi}KeJ*u=U`PB}vZeU_9#Lp9ZwH>x^IoOb7!*i;;6uCP-)P`rzguS zn##mA=?i)%0HgMUV>xa$4^=5uE@WomZgxd_gp6X;d6JI+GjQokDHYIlPO6rwBmI2RJWrx_Oyo!}571 zc6H|#rutn3&h*OROrMGryw1$BVLi^wl7@<#QJZS|_$uot7j!)F>Lkp471i zUw zh%|W$08LHv?Dq+5IUeCUL9}(;TI@wg6G!%iBp50=`32`eXT_QZ5tbFR2XGN(F&d(S zh1>V~73(_rjIOq@?B&Zf;)+!B5#W8{bTE$m|;GTKb z`t>#ScB<4|jfC%k%F4RSMS{|}1v91if%=8R)^MLC10%lTFzO{Vw{$(@B1x=#w0(80 zDQBj1kS+uGI=9qrl7XvR0s|HDF`u#f;=zca z(2pOO{Z=wOvBW2&rJ=s8qER*m(jvtM&&J-W0WzwRQyz(Ow-Mo~keBIQxHP@n=hWe; zWwKP=L<#lDYn4c3d;VUPrOOJV+jt2Dd@dFT?F1qntjW5ZF@}p7{kapF-Ronl0-&q9SjGpdrKU0e!y}p3AsK`EqIyslFmTq( z6BspGoXtu#@0U({`TB+-Xhp*vz-bJyaivnND1#uIL#pjyb;h5p@$C>=BXHLfvxWhN zSwad8CHaAH*hpxolaQHI>>{D;Y!+Au&?+3K)21II5p|TeZ;kYjOzp@ksZovq!O+_xubb?oG8DKvfF{1`zAf%t=ta- z<2&)Rc;4}J4ZZ2F!I9#ch-nUOByL&&o-Eq!ie1oD$(%-hug&-&*!OGf7pCqx8{rm- zt4|C;3U9bJ#}T-n-1oBCzqN1mlnjffLvk)lw0(^Z#c_a_nSo@C3hgzI!UVHpJp)%U z$nW4gbAi-tuLO!>Y-mSa&ZU{2ce6)il&SPKR<>5k6HWw+;4^;Km~*o|4xDTQ1VBSF z(Wqu?pV}qp5ELsL@)xP5l|k$3jCuI1WTvr0&B}&K1gIEH^R}8JIZUW$S|EfuD`_WS zuqsAeFq35XW}L`Rf7ODwCJ!0fc`aS8;)Jjksf)1YkB_)kA+gzg_O>*DaRPRLzbS@_ zG4xZU)few3MI`}@Q>4W1PppOVfR*MTm(tKiz47?P7;D#(CVtLkJ1H$U9JGW|Qhg^@ zRLaK-gb3JPmXf-e_+S@!y0A6Y_6s$9ebJ@LiZz$59>%@U)1)TS*22Mf**!KfrVsQm zgp^S`MM(WU;%f(g-8AAmV)(WqkI)pcqo<))EbqtV210{X1RXY9Qfv+F(!U88Hny&|Nf zdt7_nUq{+F>aMNT+*KGEgUvSK3OYr{vnp^lOdAVPfI7+gTxmQjbdA!TI>x1jy>8vc z6+BLxS8i)L4o9qm`pz^}1%Mp|<&D9{PC*xm*cJp}F>Tai_9yk;H4y7yzO!`73E*W{s3Z;M3F>WnDRD zcS}MA=;Exd*prczyNiuw7-A)Q1p67MrO0q1m8Pp=je__4L>c=T2?c7gLbWn4%Wmsh zD%s0B*o@A47k3@SZUOY-R4FzFKHvANMvr@vy!(siN}Wzp>QuzC;epg-%N?^ge-*DpfC*&JlP0pCVjqr z$VuIxyn%!E@Ak^qIuIf&P%vdZ)c~5uYolHQ1u~A2RWsfF zpC;)FhI#V}zrA(U`U0t*TYfUwDxA?==Zj-DGKE~9N1Rcj+lq;_5NM`Avv=)?yH7bO zD)@mf@KVBAv*)3z+iMiP)-Rfo)|Fld-VM^bq3nYLLxN8dW^z+f5dOUP^Y&LGo^0`W z0a&y-WTz=ZyAHs>YGaXDmWe@|x9(cRe14+WBAQz;+yMtVS4--}2LWCEPU~nFE>(qw z4*M~J0e-#{^gz%|0mia?^5JMw?BEM&!#CNc^T zPvbBNnPG0!7a6s(t3AM4YHmxz6*#N4bYBdOUnyY`JemIhj%mRjs_>qgto{wT=ZUtp zkevoL5&)`>MbHH=-peX(Ldx*5HRNj1iG2eNo>JJ)wT${%4;vVE1i9%}KPbAJ01<^K zvWHL|$qi^(xA(QJv3y$dYyW}RGnhw6c&iy45a#>1Z z{)9b&4gX#Q`GaMFbL*pY-1@37gXjyDbu4z0DmwGomm&gCx~wP4{3MpRvh7qoCAe0s z>^`RT+)HxT`V`z0doeP=;^$?NAxqAkywv4%FF`;V`udkS*Dz8M>@V* zEPrh3vhoiF1UC|)U#P5~Y@aWiZDQ=zeY+uV>UPdi3%Bso&)mOTqdM_!J1xKR7-?F>aylnPGrcJ5{ImNU!(EcuB*?w`Z2z#k5 zN6!FwwPQmj>5q3dKvcCkS{wG`?)xCO0A|6|8n@*;p?;5tgvB-F_U2Qo-EX9Z4qyYX zqAd>xhxY5>c>R@Q_Izo+!1thEd^7%J)Yw)f5fa$hld5BH*0mZ{+qzfl-&j+Mz2jDp zOT3eTc@jL}>qJ}v(avMVtw3hhCA$suX02TSU|-y9W1#6N1WIFxsCVhs)L;XxeYng} zX(KT!v8KSE_L_&b1(W40fL4+HF6H7Umwbg7_!kc{I^d zZ6StYro^hM`VQpTuo+p@qlJWVn>BYEb2Daz>Lq*e@W>Ksb$!c*l!36yMn?HHGdLbV|bUUN5qP@M?EZij{kN32kh>iTkjDcFJM?)8pZz2$Wez;ehkIuDRW+QjP7%2PXuA0 zeibX}(~!zXX-?a(g(6d`m4Z>1DXM{j#-S@?Kr)$7XAnbRvxxbi=1tU<PFix!2&)K%2Uqs> zK2K3t9{2O%aa5C8O@8Mn3b$#XBGy34-RrcUuChwRLZ)i?btRaf5 zVG?*7Mg3(RLpOaLLbVLPA1(i7`daAX9%UP8^|ricmWO*u@)9|%H}4tch~uDS_{=05 zw7W`c&JK3;1~LmGvlC~WcUIsBh8E7kk*UsLSue|Y-(GSmVgS`^bsY-lifyl4uqOT(+u3{H>;TqVj=?kKC%mzpZLd1* z4#V8J$M{3BuiibvqO%umqe1qP`P0uI7}@GIcu$fup2-G)Lf=Kv+T`euVGLtBG97me z{3-F*Dc|^q^8q>SSND(}vF|>Gn)8B>JAr34+{zvsZ?gTA@!5<5I9~|%)vIRnsBI`Y zS}NPclngnK1TASR@207re15ZjlB5{mXJgCMHY6=*KPv)>Y=j@h2F1)ce6w(RLv2iF z|M@20H%@F8kRb0|8Rp&}^P|*CWB%$@FdiMll58g*m3r};K9i%pGO2fy-Wr@q)}EcA z;(`~X1KZ>qRK)d4KLrKvV6Af%r$dFeovQR>gjq+0EMfs7yWbN%y?I>WOs0+3=?to3 zYp$f$>B3YRM@SDenv*Tct{6STEC;_dGR2aYn)@pV0iw$cySy>PH8|KQO0e!P&+m~B z%&W|+krbMF?xrgmyE8xWFOS+BOqf0^K7D|9R=Dl$$@<=@@1A0OIyUr}Gly;!@fjQv zj^DdB3p>DqSfE+a_`UHxiJJhF^M!7izvNXDCDWi5S?1Fe*>@Oacxy>iMdLE9K?^ ziQf_NsM-ubc%*?&YjC;u<6mn`cu{*o!=N=*Fwi;^6)>lV{EzjeWKAn~d<#rJiPfR2 zzNlkLH=vrliEhdsP9oT)N@HwBJWqq__P_uwqg+Kqh1vp5K>~VX5|fhUa-NOVGB*Tl zJVv6ClU7If7XRk`Ku&IEX6k{j1%>QF6RYIG*xKEMZ_eGu0TcVfV7u@$Zr7_Pra-q; zx+~OsBftV_L=;^GOyER52~$JlJ4n}9^JXISCbc+V69W@HUAE?>D5N|Kxn&>q$t4+6 z*}a;eLC>ghTmJSuOrtMfAeN6rzcs2$55i9U_H#3f0rnktaQ{L71oPZZR1-ytAsU#W z#$_@~2c8IahellWj=b?TqT~1Crna(eT{yh)Hb54ua|k&-dqeVYQCV7BF7pY5I6sgu zb>_Mk?%15|DANv`QLKCTxP*Ly?c>1=1X9;Up;Zy|OiK0$9t8aISQoHGokeA5e~M^=upe*0L?Yr7=LEuUmr z=0#^A;~wJ_1?F~Gmh?7rzFyn}{z1E{+w8!T@Ju#X_{7-z=haW)p7~5i9lq+1=HQ;W zvx`H~pq%Sy6Wr-rTh5EekXbyW9XJ5&I>}L!ftG2M_2J-iIyAv|yp1>rJftoEKxnS) zSnZS!GMI&nS3XL;x*oc{?w5jyn}f(0eP0h)oj(f-aXg_2t6Z8i^q4QGKfUc$ykv)h zUccnQIH993nP(=J)cg^O)F6HgOpcvUq%soy9KD@99B zWLY`U-_?{s@HUqc?I0z5imkB#jH;G2!_9vGw4_{As#8zo8J@Wsw)6SRXFNq@(F1vF zY3hMo$zGu!8zlJW7=k(6PGvC(nu0wIer3VfW0{o}Z@*O%vBH4swwcpFD8lo#v~zie zEGr3KH09LG@MO!xnAKxs&J6ZGvDqGkIfE%rpD3|(B_p5jq%#^A5br)r0g5?w+uEK{ zjy;8=MMlK@*^q?Q-fu{)g(fyZY(ClE+mpRvm11>4KMuty){_07%KcJpnb{FUwZef6^1iHr}HCKSO4eY z@dH1%X?*XMaNvM7wPxj02f)5yZesE>3*o!SGx|~p?rCU@pa&-M7H8iDpETZ9tk@H> zalxy&cNnBPgk2^cnKbBc342jrk2$c$QiIVF_Os4h8;{_f$L{kKp(ovu)+kIo(IwJR zsXd=}3A%i=RGaRb2Tc7iL_1RA9~R7{ZcEh4+*$^&WM08~Kwmld0Sl1$xG^x1_T3-l zqzDJ&omM#D&7dnRxu|=C+pq&c$%9^)@4F%1=HAw}Fb`p0=q%Hx?y{~@U~-=S7qu&2 zDJiDS?sa?8#OSXrA?wb=heVzyFd7w41s_iUiW@y*i=)y@%qK5YnKuO9m#9=EG*9$E-*jOSx71s9kH}}qrbh(xw4@kFn2)^N zuPQG##ni-0L|e|uiu3pMU7Df+NfMchOgq%2mzNuvhpiz?nYkG*0;j#7*{V^kNJh3< zkE}B4^G*%fcW@~)14}j`xK0uIMV5ef0Y7lE9f5aT=dVNp2la zeD?#Aq*)>DZJ`L@yFivc1HbNBey=J7zuvbB#TKkkCu~6*9wBX#$lgu=U38d8gOxSj z<;V%xmJ0>B*VP9JZYEwmLTSMoq@8tOA!DOmUtd9KfU?zI^yzi5`Qn6m-1GC`V`*fK)gfLU|V))&uPKU z8AH`L6UZMYe z4$_Y$;e1TcqyQasQVhtnzbW?EC_z{Mpx869vHJhe>%9WUfPhQ5#1sed9{*8Lz|KT| zeUukh6JeBAkYHAl{f+u7^xw8tNbn5^APjne&Vm1{6b1nGM+m`x>mvjO5d2Yo>B|6p zws*4lQx&K}3iv<22m1x+`~&bm!~e$NCXT*_`UN=tb+Q7(zfCD$HeZ4O1=<{g%xC;d zAsAR;0O2d(D>zJ`(ks0G2w5sW`{skdGoTmw3nA;*AR$5y3fBMa;QhO{!M(3#_@Etc zIna;ecp<0&{Ywzo4vhOd!!X z%;UxaA~7TV=TO)BTPAADceTKV|-6B?sLRniq9= z8vG%H_fPyw63)K`3IpaWK$M(x|LdQkpbhK4N?`zp&cj@h=jfU*JE7{l@3dll~L`SMnEVJbGyhM%-^a z;sVJ(@h@_2Ug$?p1mS5x={GONx);d|FYq|Yzww6)nE%xOA~O61o+cH9=ljpi{5$aY z-z`9y{<{L4Oax%(;)_;$5!m^n64I>y$>jX^0reuF?u8IaE^sT4<3EqlvLD|82L8<%}AVDy&{{!_~Q{n&s delta 47987 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 P{tQ5B1&<&s7}); Date: Mon, 20 Aug 2018 17:01:58 +0200 Subject: [PATCH 064/293] Support map / list claims --- .../main/java/com/auth0/jwt/JWTCreator.java | 90 +++++++++++++++++++ .../java/com/auth0/jwt/JWTCreatorTest.java | 29 +++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index deda9813..c33fcbe3 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -15,7 +15,9 @@ import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; /** * The JWTCreator class holds the sign method to generate a complete JWT (with Signature) from a given Header and Payload content. @@ -303,6 +305,94 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE return this; } + /** + * Add a custom Map Claim with the given items.

+ * + * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types + * Boolean, Integer, Long, Double, String and Date. + * + * @param name the Claim's name. + * @param items the Claim's key-values. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withClaim(String name, Map map) throws IllegalArgumentException { + assertNonNull(name); + if(!validateClaim(map)) { + throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date"); + } + addClaim(name, map); + return this; + } + + /** + * Add a custom List Claim with the given items. + * + * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types + * Boolean, Integer, Long, Double, String and Date. + * + * @param name the Claim's name. + * @param items the Claim's list of values. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + + public Builder withClaim(String name, List map) throws IllegalArgumentException { + assertNonNull(name); + // validate map contents + if(!validateClaim(map)) { + throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date"); + } + addClaim(name, map); + return this; + } + + private static boolean validateClaim(Map map) { + for (Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if(value != null && !isSupported(value)) { + return false; + } + + if(entry.getKey() == null || !(entry.getKey() instanceof String)) { + return false; + } + } + return true; + } + + private static boolean validateClaim(List list) { + for (Object object : list) { + if(object != null && !isSupported(object)) { + return false; + } + } + return true; + } + + @SuppressWarnings("unchecked") + private static boolean isSupported(Object value) { + if(value != null) { + if(value instanceof List) { + return validateClaim((List)value); + } else if(value instanceof Map) { + return validateClaim((Map)value); + } else { + return isBasicType(value); + } + } + return true; + } + + private static boolean isBasicType(Object value) { + Class c = value.getClass(); + + if(c.isArray()) { + return c == Integer[].class || c == Long[].class || c == String[].class; + } + return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Boolean.class; + } + /** * Creates a new JWT and signs is with the given algorithm * diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 471ff38b..629232f7 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -431,4 +431,31 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { String[] parts = jwt.split("\\."); assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); } -} + + @Test + public void shouldAcceptCustomClaimOfTypeObject() throws Exception { + Map data = new HashMap<>(); + data.put("test1", "abc"); + data.put("test2", "def"); + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJkYXRhIjp7InRlc3QyIjoiZGVmIiwidGVzdDEiOiJhYmMifX0")); + } + + @Test + public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception{ + Map data = new HashMap<>(); + data.put("test1", new UserPojo("Michael", 255)); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + +} \ No newline at end of file From 1470ba1cbec2d233ef56fa27e72d10420d54c536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Thu, 9 May 2019 12:19:56 +0200 Subject: [PATCH 065/293] Improve code coverage for JWTCreator --- .../main/java/com/auth0/jwt/JWTCreator.java | 19 +- .../java/com/auth0/jwt/JWTCreatorTest.java | 183 ++++++++++++++++++ 2 files changed, 192 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index c33fcbe3..624406b1 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -348,9 +348,10 @@ public Builder withClaim(String name, List map) throws IllegalArgumentEx } private static boolean validateClaim(Map map) { + // do not accept null values in maps for (Entry entry : map.entrySet()) { Object value = entry.getValue(); - if(value != null && !isSupported(value)) { + if(value == null || !isSupported(value)) { return false; } @@ -362,6 +363,7 @@ private static boolean validateClaim(Map map) { } private static boolean validateClaim(List list) { + // accept null values in list for (Object object : list) { if(object != null && !isSupported(object)) { return false; @@ -372,16 +374,13 @@ private static boolean validateClaim(List list) { @SuppressWarnings("unchecked") private static boolean isSupported(Object value) { - if(value != null) { - if(value instanceof List) { - return validateClaim((List)value); - } else if(value instanceof Map) { - return validateClaim((Map)value); - } else { - return isBasicType(value); - } + if(value instanceof List) { + return validateClaim((List)value); + } else if(value instanceof Map) { + return validateClaim((Map)value); + } else { + return isBasicType(value); } - return true; } private static boolean isBasicType(Object value) { diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 629232f7..7871973f 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -4,6 +4,8 @@ import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; @@ -12,9 +14,12 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.is; @@ -458,4 +463,182 @@ public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception{ .sign(Algorithm.HMAC256("secret")); } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { + Map data = new HashMap<>(); + + // simple types + data.put("string", "abc"); + data.put("integer", 1); + data.put("long", Long.MAX_VALUE); + data.put("double", 123.456d); + data.put("date", new Date(123L)); + data.put("boolean", true); + + // array types + data.put("intArray", new Integer[]{3, 5}); + data.put("longArray", new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); + data.put("stringArray", new String[]{"string"}); + + data.put("list", Arrays.asList("a", "b", "c")); + + Map sub = new HashMap<>(); + sub.put("subKey", "subValue"); + + data.put("map", sub); + + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + Map map = (Map) mapper.readValue(body, Map.class).get("data"); + + assertThat(map.get("string"), is("abc")); + assertThat(map.get("integer"), is(1)); + assertThat(map.get("long"), is(Long.MAX_VALUE)); + assertThat(map.get("double"), is(123.456d)); + assertThat(map.get("date"), is(123)); + assertThat(map.get("boolean"), is(true)); + + // array types + assertThat(map.get("intArray"), is(Arrays.asList(new Integer[]{3, 5}))); + assertThat(map.get("longArray"), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}))); + assertThat(map.get("stringArray"), is(Arrays.asList(new String[]{"string"}))); + + // list + assertThat(map.get("list"), is(Arrays.asList("a", "b", "c"))); + assertThat(map.get("map"), is(sub)); + + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { + List data = new ArrayList<>(); + + // simple types + data.add("abc"); + data.add(1); + data.add(Long.MAX_VALUE); + data.add(123.456d); + data.add(new Date(123L)); + data.add(true); + + // array types + data.add(new Integer[]{3, 5}); + data.add(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); + data.add(new String[]{"string"}); + + data.add(Arrays.asList("a", "b", "c")); + + Map sub = new HashMap<>(); + sub.put("subKey", "subValue"); + + data.add(sub); + + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + List list = (List) mapper.readValue(body, Map.class).get("data"); + + assertThat(list.get(0), is("abc")); + assertThat(list.get(1), is(1)); + assertThat(list.get(2), is(Long.MAX_VALUE)); + assertThat(list.get(3), is(123.456d)); + assertThat(list.get(4), is(123)); + assertThat(list.get(5), is(true)); + + // array types + assertThat(list.get(6), is(Arrays.asList(new Integer[]{3, 5}))); + assertThat(list.get(7), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}))); + assertThat(list.get(8), is(Arrays.asList(new String[]{"string"}))); + + // list + assertThat(list.get(9), is(Arrays.asList("a", "b", "c"))); + assertThat(list.get(10), is(sub)); + + } + + @Test + public void shouldAcceptCustomClaimForNullListItem() throws Exception{ + Map data = new HashMap<>(); + data.put("test1", Arrays.asList("a", null, "c")); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomClaimForNullMapValue() throws Exception{ + Map data = new HashMap<>(); + data.put("subKey", null); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomClaimForNullMapKey() throws Exception{ + Map data = new HashMap<>(); + data.put(null, "subValue"); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception{ + Map data = new HashMap<>(); + data.put(new Object(), "value"); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", (Map)data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception{ + List list = Arrays.asList(new UserPojo("Michael", 255)); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception{ + List list = new ArrayList<>(); + list.add(new Object[] {"test"}); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + } \ No newline at end of file From 0e5e12ff33a9f83f45c0f21e646f1a6bf6e08a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Tue, 11 Feb 2020 21:42:47 +0100 Subject: [PATCH 066/293] Initial QA changes --- .../main/java/com/auth0/jwt/JWTCreator.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 624406b1..f4be98f7 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -297,7 +297,7 @@ public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgume * @param name the Claim's name. * @param items the Claim's value. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type */ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { assertNonNull(name); @@ -306,18 +306,18 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE } /** - * Add a custom Map Claim with the given items.

- * + * Add a custom Map Claim with the given items. * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types * Boolean, Integer, Long, Double, String and Date. * * @param name the Claim's name. - * @param items the Claim's key-values. + * @param map the Claim's key-values. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type */ public Builder withClaim(String name, Map map) throws IllegalArgumentException { assertNonNull(name); + // validate map contents if(!validateClaim(map)) { throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date"); } @@ -332,18 +332,18 @@ public Builder withClaim(String name, Map map) throws IllegalArg * Boolean, Integer, Long, Double, String and Date. * * @param name the Claim's name. - * @param items the Claim's list of values. + * @param list the Claim's list of values. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type */ - public Builder withClaim(String name, List map) throws IllegalArgumentException { + public Builder withClaim(String name, List list) throws IllegalArgumentException { assertNonNull(name); - // validate map contents - if(!validateClaim(map)) { + // validate list contents + if(!validateClaim(list)) { throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date"); } - addClaim(name, map); + addClaim(name, list); return this; } @@ -351,7 +351,7 @@ private static boolean validateClaim(Map map) { // do not accept null values in maps for (Entry entry : map.entrySet()) { Object value = entry.getValue(); - if(value == null || !isSupported(value)) { + if(value == null || !isSupportedType(value)) { return false; } @@ -365,7 +365,7 @@ private static boolean validateClaim(Map map) { private static boolean validateClaim(List list) { // accept null values in list for (Object object : list) { - if(object != null && !isSupported(object)) { + if(object != null && !isSupportedType(object)) { return false; } } @@ -373,11 +373,11 @@ private static boolean validateClaim(List list) { } @SuppressWarnings("unchecked") - private static boolean isSupported(Object value) { + private static boolean isSupportedType(Object value) { if(value instanceof List) { return validateClaim((List)value); } else if(value instanceof Map) { - return validateClaim((Map)value); + return validateClaim((Map)value); } else { return isBasicType(value); } From 0130a265ca42530d1e53d50c5bbc6b0f9766f8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Thu, 13 Feb 2020 11:33:29 +0100 Subject: [PATCH 067/293] Improve Javadoc, use anon types where possible, add unit test for Map in List, formatting --- .../main/java/com/auth0/jwt/JWTCreator.java | 32 +++++---- .../java/com/auth0/jwt/JWTCreatorTest.java | 70 +++++++++++-------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index f4be98f7..7b5e3e77 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -297,7 +297,7 @@ public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgume * @param name the Claim's name. * @param items the Claim's value. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type + * @throws IllegalArgumentException if the name is null */ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { assertNonNull(name); @@ -307,15 +307,18 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE /** * Add a custom Map Claim with the given items. - * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types - * Boolean, Integer, Long, Double, String and Date. * + * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types + * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, + * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. + * {@linkplain List}s can contain null elements. + * * @param name the Claim's name. * @param map the Claim's key-values. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type + * @throws IllegalArgumentException if the name is null, or if the map contents does not validate. */ - public Builder withClaim(String name, Map map) throws IllegalArgumentException { + public Builder withClaim(String name, Map map) throws IllegalArgumentException { assertNonNull(name); // validate map contents if(!validateClaim(map)) { @@ -328,16 +331,18 @@ public Builder withClaim(String name, Map map) throws IllegalArg /** * Add a custom List Claim with the given items. * - * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types - * Boolean, Integer, Long, Double, String and Date. - * + * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types + * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, + * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. + * {@linkplain List}s can contain null elements. + * * @param name the Claim's name. * @param list the Claim's list of values. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null, or if the value is null or of an unsupported type + * @throws IllegalArgumentException if the name is null, or if the list contents does not validate. */ - public Builder withClaim(String name, List list) throws IllegalArgumentException { + public Builder withClaim(String name, List list) throws IllegalArgumentException { assertNonNull(name); // validate list contents if(!validateClaim(list)) { @@ -347,9 +352,9 @@ public Builder withClaim(String name, List list) throws IllegalArgumentE return this; } - private static boolean validateClaim(Map map) { + private static boolean validateClaim(Map map) { // do not accept null values in maps - for (Entry entry : map.entrySet()) { + for (Entry entry : map.entrySet()) { Object value = entry.getValue(); if(value == null || !isSupportedType(value)) { return false; @@ -372,12 +377,11 @@ private static boolean validateClaim(List list) { return true; } - @SuppressWarnings("unchecked") private static boolean isSupportedType(Object value) { if(value instanceof List) { return validateClaim((List)value); } else if(value instanceof Map) { - return validateClaim((Map)value); + return validateClaim((Map)value); } else { return isBasicType(value); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 7871973f..7df869c1 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -436,9 +436,9 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { String[] parts = jwt.split("\\."); assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); } - + @Test - public void shouldAcceptCustomClaimOfTypeObject() throws Exception { + public void shouldAcceptCustomClaimOfTypeMap() throws Exception { Map data = new HashMap<>(); data.put("test1", "abc"); data.put("test2", "def"); @@ -450,25 +450,24 @@ public void shouldAcceptCustomClaimOfTypeObject() throws Exception { String[] parts = jwt.split("\\."); assertThat(parts[1], is("eyJkYXRhIjp7InRlc3QyIjoiZGVmIiwidGVzdDEiOiJhYmMifX0")); } - + @Test public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception{ - Map data = new HashMap<>(); + Map data = new HashMap<>(); data.put("test1", new UserPojo("Michael", 255)); - + exception.expect(IllegalArgumentException.class); - JWTCreator.init() + JWTCreator.init() .withClaim("pojo", data) .sign(Algorithm.HMAC256("secret")); } - - + @SuppressWarnings("unchecked") - @Test + @Test public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { Map data = new HashMap<>(); - + // simple types data.put("string", "abc"); data.put("integer", 1); @@ -476,7 +475,7 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { data.put("double", 123.456d); data.put("date", new Date(123L)); data.put("boolean", true); - + // array types data.put("intArray", new Integer[]{3, 5}); data.put("longArray", new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); @@ -486,7 +485,7 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { Map sub = new HashMap<>(); sub.put("subKey", "subValue"); - + data.put("map", sub); String jwt = JWTCreator.init() @@ -506,7 +505,7 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { assertThat(map.get("double"), is(123.456d)); assertThat(map.get("date"), is(123)); assertThat(map.get("boolean"), is(true)); - + // array types assertThat(map.get("intArray"), is(Arrays.asList(new Integer[]{3, 5}))); assertThat(map.get("longArray"), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}))); @@ -517,12 +516,12 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { assertThat(map.get("map"), is(sub)); } - + @SuppressWarnings("unchecked") - @Test + @Test public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { List data = new ArrayList<>(); - + // simple types data.add("abc"); data.add(1); @@ -540,7 +539,7 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { Map sub = new HashMap<>(); sub.put("subKey", "subValue"); - + data.add(sub); String jwt = JWTCreator.init() @@ -571,7 +570,7 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { assertThat(list.get(10), is(sub)); } - + @Test public void shouldAcceptCustomClaimForNullListItem() throws Exception{ Map data = new HashMap<>(); @@ -581,7 +580,7 @@ public void shouldAcceptCustomClaimForNullListItem() throws Exception{ .withClaim("pojo", data) .sign(Algorithm.HMAC256("secret")); } - + @Test public void shouldRefuseCustomClaimForNullMapValue() throws Exception{ Map data = new HashMap<>(); @@ -593,7 +592,7 @@ public void shouldRefuseCustomClaimForNullMapValue() throws Exception{ .withClaim("pojo", data) .sign(Algorithm.HMAC256("secret")); } - + @Test public void shouldRefuseCustomClaimForNullMapKey() throws Exception{ Map data = new HashMap<>(); @@ -604,8 +603,9 @@ public void shouldRefuseCustomClaimForNullMapKey() throws Exception{ JWTCreator.init() .withClaim("pojo", data) .sign(Algorithm.HMAC256("secret")); - } - + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception{ Map data = new HashMap<>(); @@ -617,28 +617,42 @@ public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception{ .withClaim("pojo", (Map)data) .sign(Algorithm.HMAC256("secret")); } - + @Test public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception{ - List list = Arrays.asList(new UserPojo("Michael", 255)); + List list = Arrays.asList(new UserPojo("Michael", 255)); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() throws Exception{ + List list = Arrays.asList(new UserPojo("Michael", 255)); + Map data = new HashMap<>(); + data.put("someList", list); + exception.expect(IllegalArgumentException.class); JWTCreator.init() .withClaim("list", list) .sign(Algorithm.HMAC256("secret")); } - + @Test public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception{ - List list = new ArrayList<>(); + List list = new ArrayList<>(); list.add(new Object[] {"test"}); - + exception.expect(IllegalArgumentException.class); JWTCreator.init() .withClaim("list", list) .sign(Algorithm.HMAC256("secret")); } - + } \ No newline at end of file From 373bb6bbecfca03c2981fef0714e865be55d9006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Mon, 20 Aug 2018 17:09:11 +0200 Subject: [PATCH 068/293] Support verification of Long[] datatype like in JWTCreator --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 15 +++++++++++++++ .../com/auth0/jwt/interfaces/Verification.java | 13 ++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index ff25db09..67ab80b1 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -168,6 +168,21 @@ public Verification withArrayClaim(String name, Integer... items) throws Illegal requireClaim(name, items); return this; } + + /** + * Require a specific Array Claim to contain at least the given items. + * + * @param name the Claim's name. + * @param items the items the Claim must contain. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ + @Override + public Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException { + assertNonNull(name); + requireClaim(name, items); + return this; + } @Override public JWTVerifier build() { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 511e59b7..f4fd9d8b 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -160,15 +160,18 @@ public interface Verification { */ Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; - /** - * Skip the Issued At ("iat") date verification. By default, the verification is performed. - */ - Verification ignoreIssuedAt(); - /** * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. * * @return a new JWTVerifier instance. */ + Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException; + + + /** + * Skip the Issued At ("iat") date verification. By default, the verification is performed. + */ + Verification ignoreIssuedAt(); + JWTVerifier build(); } From 9f5cb2606167e5597038ef40c773aaed53deb5e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Tue, 29 Jan 2019 14:36:59 +0100 Subject: [PATCH 069/293] Add unit tests and handling of integer vs long datatypes --- .../main/java/com/auth0/jwt/JWTVerifier.java | 18 ++++++++++- .../java/com/auth0/jwt/JWTVerifierTest.java | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 67ab80b1..8a2598f0 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -325,7 +325,23 @@ private void assertValidClaim(Claim claim, String claimName, Object value) { } else if (value instanceof Date) { isValid = value.equals(claim.asDate()); } else if (value instanceof Object[]) { - List claimArr = Arrays.asList(claim.as(Object[].class)); + List claimArr; + Object[] claimAsObject = claim.as(Object[].class); + + // Jackson uses 'natural' mapping which uses Integer if value fits in 32 bits. + if(value instanceof Long[]) { + // convert Integers to Longs for comparison with equals + claimArr = new ArrayList<>(claimAsObject.length); + for(Object cao : claimAsObject) { + if(cao instanceof Integer) { + claimArr.add(((Integer)cao).longValue()); + } else { + claimArr.add(cao); + } + } + } else { + claimArr = Arrays.asList(claim.as(Object[].class)); + } List valueArr = Arrays.asList((Object[]) value); isValid = claimArr.containsAll(valueArr); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index e297c59c..8d2a621c 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -326,6 +326,38 @@ public void shouldValidateCustomArrayClaimOfTypeInteger() throws Exception { assertThat(jwt, is(notNullValue())); } + @Test + public void shouldValidateCustomArrayClaimOfTypeLong() throws Exception { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbNTAwMDAwMDAwMDAxLDUwMDAwMDAwMDAwMiw1MDAwMDAwMDAwMDNdfQ.vzV7S0gbV9ZAVxChuIt4XZuSVTxMH536rFmoHzxmayM"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 500000000001L, 500000000002L, 500000000003L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() throws Exception { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 1L, 2L, 3L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() throws Exception { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSw1MDAwMDAwMDAwMDIsNTAwMDAwMDAwMDAzXX0.PQjb2rPPpYjM2sItZEzZcjS2YbfPCp6xksTSPjpjTQA"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 1L, 500000000002L, 500000000003L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } // Generic Delta @SuppressWarnings("RedundantCast") @Test From 5fa3fe310f69fd31a4349f352a9a87218bf56edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Skj=C3=B8lberg?= Date: Fri, 14 Feb 2020 18:53:54 +0100 Subject: [PATCH 070/293] Fix Javadoc --- .../com/auth0/jwt/interfaces/Verification.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index f4fd9d8b..dec868fb 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -161,17 +161,25 @@ public interface Verification { Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; /** - * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. + * Require a specific Array Claim to contain at least the given items. * - * @return a new JWTVerifier instance. + * @param name the Claim's name. + * @param items the items the Claim must contain. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. */ + Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException; - /** * Skip the Issued At ("iat") date verification. By default, the verification is performed. */ Verification ignoreIssuedAt(); + /** + * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. + * + * @return a new JWTVerifier instance. + */ JWTVerifier build(); } From cbe8eebb53c7c0519a46b061b471069ce59cee88 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 14 Feb 2020 17:06:26 -0300 Subject: [PATCH 071/293] fix NPE when missing expected array claim --- .../main/java/com/auth0/jwt/JWTVerifier.java | 18 ++++++------- .../java/com/auth0/jwt/JWTVerifierTest.java | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 8a2598f0..b9f745e9 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -168,7 +168,7 @@ public Verification withArrayClaim(String name, Integer... items) throws Illegal requireClaim(name, items); return this; } - + /** * Require a specific Array Claim to contain at least the given items. * @@ -178,11 +178,11 @@ public Verification withArrayClaim(String name, Integer... items) throws Illegal * @throws IllegalArgumentException if the name is null. */ @Override - public Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException { + public Verification withArrayClaim(String name, Long... items) throws IllegalArgumentException { assertNonNull(name); requireClaim(name, items); return this; - } + } @Override public JWTVerifier build() { @@ -220,7 +220,7 @@ private void addLeewayToDateClaims() { if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); } - if(ignoreIssuedAt) { + if (ignoreIssuedAt) { claims.remove(PublicClaims.ISSUED_AT); return; } @@ -329,18 +329,18 @@ private void assertValidClaim(Claim claim, String claimName, Object value) { Object[] claimAsObject = claim.as(Object[].class); // Jackson uses 'natural' mapping which uses Integer if value fits in 32 bits. - if(value instanceof Long[]) { + if (value instanceof Long[]) { // convert Integers to Longs for comparison with equals claimArr = new ArrayList<>(claimAsObject.length); - for(Object cao : claimAsObject) { - if(cao instanceof Integer) { - claimArr.add(((Integer)cao).longValue()); + for (Object cao : claimAsObject) { + if (cao instanceof Integer) { + claimArr.add(((Integer) cao).longValue()); } else { claimArr.add(cao); } } } else { - claimArr = Arrays.asList(claim.as(Object[].class)); + claimArr = claim.isNull() ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); } List valueArr = Arrays.asList((Object[]) value); isValid = claimArr.containsAll(valueArr); diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 8d2a621c..b635e08b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -170,6 +170,28 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { .withClaim(null, "value"); } + @Test + public void shouldThrowWhenExpectedArrayClaimIsMissing() throws Exception { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'missing' value doesn't match the required one."); + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcnJheSI6WzEsMiwzXX0.wKNFBcMdwIpdF9rXRxvexrzSM6umgSFqRO1WZj992YM"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("missing", 1, 2, 3) + .build() + .verify(token); + } + + @Test + public void shouldThrowWhenExpectedClaimIsMissing() throws Exception { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'missing' value doesn't match the required one."); + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbSI6InRleHQifQ.aZ27Ze35VvTqxpaSIK5ZcnYHr4SrvANlUbDR8fw9qsQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("missing", "text") + .build() + .verify(token); + } + @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeString() throws Exception { exception.expect(InvalidClaimException.class); @@ -546,8 +568,8 @@ public void shouldThrowOnNegativeNotBeforeLeeway() throws Exception { .acceptNotBefore(-1); } -// Issued At with future date - @Test (expected = InvalidClaimException.class) + // Issued At with future date + @Test(expected = InvalidClaimException.class) public void shouldThrowOnFutureIssuedAt() throws Exception { Clock clock = mock(Clock.class); when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); From 8e02a2efb466ca743dc10f7f746ffbd36d6c465b Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 14 Feb 2020 17:11:08 -0300 Subject: [PATCH 072/293] code-formatting changes --- .../main/java/com/auth0/jwt/JWTCreator.java | 46 ++++---- .../main/java/com/auth0/jwt/JWTVerifier.java | 8 -- .../com/auth0/jwt/interfaces/JWTVerifier.java | 34 +++--- .../java/com/auth0/jwt/JWTCreatorTest.java | 103 ++++++++---------- .../java/com/auth0/jwt/JWTVerifierTest.java | 1 + 5 files changed, 89 insertions(+), 103 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 7b5e3e77..3cd0b7ea 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -307,81 +307,81 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE /** * Add a custom Map Claim with the given items. - * + *

* Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. * {@linkplain List}s can contain null elements. * - * @param name the Claim's name. - * @param map the Claim's key-values. + * @param name the Claim's name. + * @param map the Claim's key-values. * @return this same Builder instance. * @throws IllegalArgumentException if the name is null, or if the map contents does not validate. */ public Builder withClaim(String name, Map map) throws IllegalArgumentException { assertNonNull(name); // validate map contents - if(!validateClaim(map)) { + if (!validateClaim(map)) { throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date"); } addClaim(name, map); return this; - } - + } + /** * Add a custom List Claim with the given items. - * + *

* Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. * {@linkplain List}s can contain null elements. * - * @param name the Claim's name. + * @param name the Claim's name. * @param list the Claim's list of values. * @return this same Builder instance. * @throws IllegalArgumentException if the name is null, or if the list contents does not validate. */ - + public Builder withClaim(String name, List list) throws IllegalArgumentException { assertNonNull(name); // validate list contents - if(!validateClaim(list)) { + if (!validateClaim(list)) { throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date"); } addClaim(name, list); return this; - } + } private static boolean validateClaim(Map map) { // do not accept null values in maps for (Entry entry : map.entrySet()) { Object value = entry.getValue(); - if(value == null || !isSupportedType(value)) { + if (value == null || !isSupportedType(value)) { return false; } - - if(entry.getKey() == null || !(entry.getKey() instanceof String)) { + + if (entry.getKey() == null || !(entry.getKey() instanceof String)) { return false; } } return true; } - + private static boolean validateClaim(List list) { // accept null values in list for (Object object : list) { - if(object != null && !isSupportedType(object)) { + if (object != null && !isSupportedType(object)) { return false; } } return true; - } + } private static boolean isSupportedType(Object value) { - if(value instanceof List) { - return validateClaim((List)value); - } else if(value instanceof Map) { - return validateClaim((Map)value); + if (value instanceof List) { + return validateClaim((List) value); + } else if (value instanceof Map) { + return validateClaim((Map) value); } else { return isBasicType(value); } @@ -389,8 +389,8 @@ private static boolean isSupportedType(Object value) { private static boolean isBasicType(Object value) { Class c = value.getClass(); - - if(c.isArray()) { + + if (c.isArray()) { return c == Integer[].class || c == Long[].class || c == String[].class; } return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Boolean.class; diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index b9f745e9..0f56b3da 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -169,14 +169,6 @@ public Verification withArrayClaim(String name, Integer... items) throws Illegal return this; } - /** - * Require a specific Array Claim to contain at least the given items. - * - * @param name the Claim's name. - * @param items the items the Claim must contain. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withArrayClaim(String name, Long... items) throws IllegalArgumentException { assertNonNull(name); diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index 140af8e6..a335a83e 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -4,22 +4,22 @@ public interface JWTVerifier { - - /** - * Performs the verification against the given Token - * - * @param token to verify. - * @return a verified and decoded JWT. - * @throws JWTVerificationException if any of the verification steps fail - */ - DecodedJWT verify(String token) throws JWTVerificationException; - /** - * Performs the verification against the given decoded JWT - * - * @param jwt to verify. - * @return a verified and decoded JWT. - * @throws JWTVerificationException if any of the verification steps fail - */ - DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException; + /** + * Performs the verification against the given Token + * + * @param token to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ + DecodedJWT verify(String token) throws JWTVerificationException; + + /** + * Performs the verification against the given decoded JWT + * + * @param jwt to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ + DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException; } diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 7df869c1..0ab088c2 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -5,7 +5,6 @@ import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; import com.fasterxml.jackson.databind.ObjectMapper; - import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; @@ -14,13 +13,7 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -63,8 +56,8 @@ public void shouldAddHeaderClaim() throws Exception { @Test public void shouldReturnBuilderIfNullMapIsProvided() throws Exception { String signed = JWTCreator.init() - .withHeader(null) - .sign(Algorithm.HMAC256("secret")); + .withHeader(null) + .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); } @@ -75,9 +68,9 @@ public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() throws header.put(PublicClaims.KEY_ID, "xyz"); String signed = JWTCreator.init() - .withKeyId("abc") - .withHeader(header) - .sign(Algorithm.HMAC256("secret")); + .withKeyId("abc") + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); @@ -91,9 +84,9 @@ public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() throws Exce header.put(PublicClaims.KEY_ID, "xyz"); String signed = JWTCreator.init() - .withHeader(header) - .withKeyId("abc") - .sign(Algorithm.HMAC256("secret")); + .withHeader(header) + .withKeyId("abc") + .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); @@ -108,9 +101,9 @@ public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { header.put("test2", "isSet"); String signed = JWTCreator.init() - .withKeyId("test") - .withHeader(header) - .sign(Algorithm.HMAC256("secret")); + .withKeyId("test") + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); @@ -452,15 +445,15 @@ public void shouldAcceptCustomClaimOfTypeMap() throws Exception { } @Test - public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception{ + public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception { Map data = new HashMap<>(); data.put("test1", new UserPojo("Michael", 255)); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", data) - .sign(Algorithm.HMAC256("secret")); + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); } @SuppressWarnings("unchecked") @@ -494,7 +487,7 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); Map map = (Map) mapper.readValue(body, Map.class).get("data"); @@ -529,7 +522,7 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { data.add(123.456d); data.add(new Date(123L)); data.add(true); - + // array types data.add(new Integer[]{3, 5}); data.add(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); @@ -548,18 +541,18 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); List list = (List) mapper.readValue(body, Map.class).get("data"); - + assertThat(list.get(0), is("abc")); assertThat(list.get(1), is(1)); assertThat(list.get(2), is(Long.MAX_VALUE)); assertThat(list.get(3), is(123.456d)); assertThat(list.get(4), is(123)); assertThat(list.get(5), is(true)); - + // array types assertThat(list.get(6), is(Arrays.asList(new Integer[]{3, 5}))); assertThat(list.get(7), is(Arrays.asList(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}))); @@ -572,87 +565,87 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { } @Test - public void shouldAcceptCustomClaimForNullListItem() throws Exception{ + public void shouldAcceptCustomClaimForNullListItem() throws Exception { Map data = new HashMap<>(); data.put("test1", Arrays.asList("a", null, "c")); - + JWTCreator.init() - .withClaim("pojo", data) - .sign(Algorithm.HMAC256("secret")); + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomClaimForNullMapValue() throws Exception{ + public void shouldRefuseCustomClaimForNullMapValue() throws Exception { Map data = new HashMap<>(); data.put("subKey", null); - + exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", data) - .sign(Algorithm.HMAC256("secret")); + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomClaimForNullMapKey() throws Exception{ + public void shouldRefuseCustomClaimForNullMapKey() throws Exception { Map data = new HashMap<>(); data.put(null, "subValue"); - + exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", data) - .sign(Algorithm.HMAC256("secret")); + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) @Test - public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception{ + public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception { Map data = new HashMap<>(); data.put(new Object(), "value"); - + exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", (Map)data) - .sign(Algorithm.HMAC256("secret")); + .withClaim("pojo", (Map) data) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception{ + public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception { List list = Arrays.asList(new UserPojo("Michael", 255)); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("list", list) - .sign(Algorithm.HMAC256("secret")); + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() throws Exception{ + public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() throws Exception { List list = Arrays.asList(new UserPojo("Michael", 255)); - + Map data = new HashMap<>(); data.put("someList", list); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("list", list) - .sign(Algorithm.HMAC256("secret")); + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception{ + public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception { List list = new ArrayList<>(); - list.add(new Object[] {"test"}); + list.add(new Object[]{"test"}); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("list", list) - .sign(Algorithm.HMAC256("secret")); + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index b635e08b..c60e31db 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -380,6 +380,7 @@ public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() assertThat(jwt, is(notNullValue())); } + // Generic Delta @SuppressWarnings("RedundantCast") @Test From dd164c656c5deb2ca0d52883f09425bfa1f7760c Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 14 Feb 2020 17:34:12 -0300 Subject: [PATCH 073/293] Release 3.10.0 --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e03dd703..941b9c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [3.10.0](https://github.com/auth0/java-jwt/tree/3.10.0) (2020-02-14) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.9.0...3.10.0) +**Closed issues** +- NullPointerException when the claim doesn't exist in the token [\#384](https://github.com/auth0/java-jwt/issues/384) + +**Added** +- Add Javadoc URL and badge to the README [\#382](https://github.com/auth0/java-jwt/pull/382) ([lbalmaceda](https://github.com/lbalmaceda)) +- Allow to customize the typ header claim [\#381](https://github.com/auth0/java-jwt/pull/381) ([lbalmaceda](https://github.com/lbalmaceda)) +- JWTCreator for basic types [\#282](https://github.com/auth0/java-jwt/pull/282) ([skjolber](https://github.com/skjolber)) +- Support verification of Long[] datatype like in JWTCreator [\#278](https://github.com/auth0/java-jwt/pull/278) ([skjolber](https://github.com/skjolber)) + +**Changed** +- Update to Gradle 6.1.1 [\#389](https://github.com/auth0/java-jwt/pull/389) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Handle missing expected array claim [\#393](https://github.com/auth0/java-jwt/pull/393) ([lbalmaceda](https://github.com/lbalmaceda)) +- Update tests to use valid Base64 URL-encoded tokens [\#386](https://github.com/auth0/java-jwt/pull/386) ([jimmyjames](https://github.com/jimmyjames)) + ## [3.9.0](https://github.com/auth0/java-jwt/tree/3.9.0) (2020-01-02) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.3...3.9.0) diff --git a/README.md b/README.md index b63599a2..344ec66b 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.9.0 + 3.10.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.9.0' +implementation 'com.auth0:java-jwt:3.10.0' ``` ## Available Algorithms From 7718330f6155035bc43038d4ff4ccc19171a6131 Mon Sep 17 00:00:00 2001 From: Claude Gex Date: Fri, 28 Feb 2020 10:41:34 +0100 Subject: [PATCH 074/293] Update build.gradle update to jackson-databind 2.10.2 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 36faf197..7ed92c8e 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0.pr3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.2' implementation 'commons-codec:commons-codec:1.12' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From f50bacf29e98c23bdff8dd3136cc6ef86abecd5a Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 13 Mar 2020 14:42:53 -0500 Subject: [PATCH 075/293] Update dependencies (#407) --- lib/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 7ed92c8e..dcd0d615 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,8 +34,8 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.2' - implementation 'commons-codec:commons-codec:1.12' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.3' + implementation 'commons-codec:commons-codec:1.14' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' testImplementation 'net.jodah:concurrentunit:0.4.3' From a5da770f88101fa292760898891ff5f622b10263 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 13 Mar 2020 15:08:57 -0500 Subject: [PATCH 076/293] Release 3.10.1 (#408) --- CHANGELOG.md | 9 +++++++++ README.md | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 941b9c87..d509a704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## [3.10.1](https://github.com/auth0/java-jwt/tree/3.10.1) (2020-03-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.0...3.10.1) + +**Changed** +- Update Jackson and Commons Codec dependencies [\#407](https://github.com/auth0/java-jwt/pull/407) ([jimmyjames](https://github.com/jimmyjames)) + +**Security** +- Update jackson-databind to 2.10.2 [\#399](https://github.com/auth0/java-jwt/pull/399) ([gexclaude](https://github.com/gexclaude)) + ## [3.10.0](https://github.com/auth0/java-jwt/tree/3.10.0) (2020-02-14) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.9.0...3.10.0) **Closed issues** diff --git a/README.md b/README.md index 344ec66b..66849932 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.10.0 + 3.10.1 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.10.0' +implementation 'com.auth0:java-jwt:3.10.1' ``` ## Available Algorithms From 45036b98d3cd35e7540237797fecd7f89c2d3b64 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 25 Mar 2020 16:58:09 -0500 Subject: [PATCH 077/293] Check varargs null values in JWTVerifier (#412) * Check varargs null values in JWTVerifier * Review feedback - Rename method for clarity --- .../main/java/com/auth0/jwt/JWTVerifier.java | 18 +++- .../java/com/auth0/jwt/JWTVerifierTest.java | 89 ++++++++++++++++++- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 0f56b3da..24e109a4 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -57,7 +57,7 @@ public static class BaseVerification implements Verification { @Override public Verification withIssuer(String... issuer) { - requireClaim(PublicClaims.ISSUER, issuer == null ? null : Arrays.asList(issuer)); + requireClaim(PublicClaims.ISSUER, isNullOrEmpty(issuer) ? null : Arrays.asList(issuer)); return this; } @@ -69,7 +69,7 @@ public Verification withSubject(String subject) { @Override public Verification withAudience(String... audience) { - requireClaim(PublicClaims.AUDIENCE, audience == null ? null : Arrays.asList(audience)); + requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); return this; } @@ -230,6 +230,20 @@ private void requireClaim(String name, Object value) { } } + private static boolean isNullOrEmpty(String[] args) { + if (args == null || args.length == 0) { + return true; + } + boolean isAllNull = true; + for (String arg: args) { + if (arg != null) { + isAllNull = false; + break; + } + } + return isAllNull; + } + /** * Perform the verification against the given Token, using any previous configured options. diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index c60e31db..aec70c70 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -125,7 +126,7 @@ public void shouldValidateAudience() throws Exception { .verify(tokenArr); assertThat(jwtArr, is(notNullValue())); - } + } @Test public void shouldAcceptPartialAudience() throws Exception { @@ -150,12 +151,53 @@ public void shouldThrowOnInvalidAudience() throws Exception { .verify(token); } + @Test + public void shouldRemoveAudienceWhenPassingNullReference() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withAudience((String) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAudience((String[]) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAudience() + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + String emptyAud = " "; + verifier = JWTVerifier.init(algorithm) + .withAudience(emptyAud) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); + } + @Test public void shouldRemoveAudienceWhenPassingNull() throws Exception { Algorithm algorithm = mock(Algorithm.class); JWTVerifier verifier = JWTVerifier.init(algorithm) .withAudience("John") - .withAudience(null) + .withAudience((String) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAudience("John") + .withAudience((String[]) null) .build(); assertThat(verifier.claims, is(notNullValue())); @@ -674,11 +716,52 @@ public void shouldRemoveClaimWhenPassingNull() throws Exception { Algorithm algorithm = mock(Algorithm.class); JWTVerifier verifier = JWTVerifier.init(algorithm) .withIssuer("iss") - .withIssuer(null) + .withIssuer((String) null) .build(); assertThat(verifier.claims, is(notNullValue())); assertThat(verifier.claims, not(hasKey("iss"))); + + verifier = JWTVerifier.init(algorithm) + .withIssuer("iss") + .withIssuer((String[]) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("iss"))); + } + + @Test + public void shouldRemoveIssuerWhenPassingNullReference() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withIssuer((String) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("iss"))); + + verifier = JWTVerifier.init(algorithm) + .withIssuer((String[]) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("iss"))); + + verifier = JWTVerifier.init(algorithm) + .withIssuer() + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("iss"))); + + String emptyIss = " "; + verifier = JWTVerifier.init(algorithm) + .withIssuer(emptyIss) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, hasEntry("iss", Collections.singletonList(emptyIss))); } @Test From f2d95fa7bf1880506680262bcff6e12673dbb133 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 27 Mar 2020 14:05:11 -0500 Subject: [PATCH 078/293] JavaDoc fix (#413) --- lib/src/main/java/com/auth0/jwt/interfaces/Verification.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index dec868fb..72ae35d2 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -173,6 +173,8 @@ public interface Verification { /** * Skip the Issued At ("iat") date verification. By default, the verification is performed. + * + * @return this same Verification instance. */ Verification ignoreIssuedAt(); From 720ba889c83cb0d1e92c7bfefd191a1e8a498bae Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 27 Mar 2020 14:18:32 -0500 Subject: [PATCH 079/293] Release 3.10.2 --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d509a704..acb7bc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [3.10.2](https://github.com/auth0/java-jwt/tree/3.10.2) (2020-03-27) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.1...3.10.2) + +**Fixed** +- JavaDoc fix [\#413](https://github.com/auth0/java-jwt/pull/413) ([jimmyjames](https://github.com/jimmyjames)) +- Check varargs null values in JWTVerifier [\#412](https://github.com/auth0/java-jwt/pull/412) ([jimmyjames](https://github.com/jimmyjames)) + ## [3.10.1](https://github.com/auth0/java-jwt/tree/3.10.1) (2020-03-13) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.0...3.10.1) diff --git a/README.md b/README.md index 66849932..e466d2dd 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.10.1 + 3.10.2 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.10.1' +implementation 'com.auth0:java-jwt:3.10.2' ``` ## Available Algorithms From a81f3076ee02d5bfa305597b5d2aa2341c2657a8 Mon Sep 17 00:00:00 2001 From: danila_varatyntsev Date: Thu, 23 Apr 2020 10:58:11 +0300 Subject: [PATCH 080/293] Fixed an NPE on null map and list claims --- .../main/java/com/auth0/jwt/JWTCreator.java | 4 +-- .../java/com/auth0/jwt/JWTCreatorTest.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 3cd0b7ea..0b4da54b 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -321,7 +321,7 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE public Builder withClaim(String name, Map map) throws IllegalArgumentException { assertNonNull(name); // validate map contents - if (!validateClaim(map)) { + if (map != null && !validateClaim(map)) { throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date"); } addClaim(name, map); @@ -345,7 +345,7 @@ public Builder withClaim(String name, Map map) throws IllegalArgument public Builder withClaim(String name, List list) throws IllegalArgumentException { assertNonNull(name); // validate list contents - if (!validateClaim(list)) { + if (list != null && !validateClaim(list)) { throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date"); } addClaim(name, list); diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 0ab088c2..c8dcae8d 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -15,6 +15,7 @@ import java.security.interfaces.RSAPrivateKey; import java.util.*; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; @@ -574,6 +575,40 @@ public void shouldAcceptCustomClaimForNullListItem() throws Exception { .sign(Algorithm.HMAC256("secret")); } + @Test + @SuppressWarnings("unchecked") + public void shouldAcceptCustomClaimWithNullMapAndRemoveClaim() throws Exception { + String jwt = JWTCreator.init() + .withClaim("map", "stubValue") + .withClaim("map", (Map) null) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + Map map = (Map) mapper.readValue(body, Map.class); + assertThat(map, anEmptyMap()); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAcceptCustomClaimWithNullListAndRemoveClaim() throws Exception { + String jwt = JWTCreator.init() + .withClaim("list", "stubValue") + .withClaim("list", (List) null) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + Map map = (Map) mapper.readValue(body, Map.class); + assertThat(map, anEmptyMap()); + } + @Test public void shouldRefuseCustomClaimForNullMapValue() throws Exception { Map data = new HashMap<>(); From 79ed2431f0f3b35bff1e1a461ba1a240ca4aa8f2 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 24 Apr 2020 14:24:56 -0500 Subject: [PATCH 081/293] Release 3.10.3 (#418) --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acb7bc52..21095ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.10.3](https://github.com/auth0/java-jwt/tree/3.10.3) (2020-04-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.2...3.10.3) + +**Fixed** +- Fixed an NPE on null map and list claims [\#417](https://github.com/auth0/java-jwt/pull/417) ([Vorotyntsev](https://github.com/Vorotyntsev)) + ## [3.10.2](https://github.com/auth0/java-jwt/tree/3.10.2) (2020-03-27) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.1...3.10.2) diff --git a/README.md b/README.md index e466d2dd..a0685729 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.10.2 + 3.10.3 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.10.2' +implementation 'com.auth0:java-jwt:3.10.3' ``` ## Available Algorithms From b3dd755ae208ffa9d532643d0454cb80b1f61b33 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 17:36:50 -0300 Subject: [PATCH 082/293] make PayloadImpl keep an unmodifiable list of audiences --- .../main/java/com/auth0/jwt/impl/PayloadImpl.java | 2 +- .../java/com/auth0/jwt/impl/PayloadImplTest.java | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index 2c5558f2..cf678f81 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -30,7 +30,7 @@ class PayloadImpl implements Payload, Serializable { PayloadImpl(String issuer, String subject, List audience, Date expiresAt, Date notBefore, Date issuedAt, String jwtId, Map tree, ObjectReader objectReader) { this.issuer = issuer; this.subject = subject; - this.audience = audience; + this.audience = audience != null ? Collections.unmodifiableList(audience) : null; this.expiresAt = expiresAt; this.notBefore = notBefore; this.issuedAt = issuedAt; diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java index 56654427..3c604a30 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -13,10 +13,7 @@ import org.junit.rules.ExpectedException; import org.mockito.Mockito; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; import static org.hamcrest.MatcherAssert.assertThat; @@ -39,7 +36,7 @@ public class PayloadImplTest { public void setUp() throws Exception { mapper = getDefaultObjectMapper(); objectReader = mapper.reader(); - + expiresAt = Mockito.mock(Date.class); notBefore = Mockito.mock(Date.class); issuedAt = Mockito.mock(Date.class); @@ -56,6 +53,13 @@ public void shouldHaveUnmodifiableTree() throws Exception { payload.getTree().put("something", null); } + @Test + public void shouldHaveUnmodifiableAudience() throws Exception { + exception.expect(UnsupportedOperationException.class); + PayloadImpl payload = new PayloadImpl(null, null, new ArrayList(), null, null, null, null, null, objectReader); + payload.getAudience().add("something"); + } + @Test public void shouldGetIssuer() throws Exception { assertThat(payload, is(notNullValue())); From 4d2bfdbff34bc76c9793931d0527a75f9b8216be Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:02:07 -0300 Subject: [PATCH 083/293] add failing test case for date reference manipulation --- .../java/com/auth0/jwt/JWTVerifierTest.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index aec70c70..d57ca187 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -15,10 +15,10 @@ import java.util.HashMap; import java.util.Map; +import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class JWTVerifierTest { @@ -126,7 +126,7 @@ public void shouldValidateAudience() throws Exception { .verify(tokenArr); assertThat(jwtArr, is(notNullValue())); - } + } @Test public void shouldAcceptPartialAudience() throws Exception { @@ -505,6 +505,23 @@ public void shouldThrowOnNegativeCustomLeeway() throws Exception { .acceptLeeway(-1); } + @Test + public void shouldNotModifyOriginalClockDateWhenVerifying() throws Exception { + Clock clock = mock(Clock.class); + Date clockDate = spy(new Date(DATE_TOKEN_MS_VALUE)); + when(clock.getToday()).thenReturn(clockDate); + + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + JWTVerifier verifier = verification + .build(clock); + + DecodedJWT jwt = verifier.verify(token); + assertThat(jwt, is(notNullValue())); + + verify(clockDate, never()).setTime(anyLong()); + } + // Expires At @Test public void shouldValidateExpiresAtWithLeeway() throws Exception { From 7d0111cf247ff5e8828f67dc837ac1ccdd056f1b Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:02:29 -0300 Subject: [PATCH 084/293] fix date manipulation by creating a new Date instance --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 24e109a4..a5fab486 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -235,7 +235,7 @@ private static boolean isNullOrEmpty(String[] args) { return true; } boolean isAllNull = true; - for (String arg: args) { + for (String arg : args) { if (arg != null) { isAllNull = false; break; @@ -364,7 +364,7 @@ private void assertValidStringClaim(String claimName, String value, String expec } private void assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) { - Date today = clock.getToday(); + Date today = new Date(clock.getToday().getTime()); today.setTime(today.getTime() / 1000 * 1000); // truncate millis if (shouldBeFuture) { assertDateIsFuture(date, leeway, today); From 3d3c794f3dbe1513106b09143b1ae12c91dda1ca Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:10:03 -0300 Subject: [PATCH 085/293] add failing test case for array manipulation in HMACAlgorithm --- .../com/auth0/jwt/algorithms/HMACAlgorithmTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index b70c8633..22d29258 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -9,6 +9,7 @@ import org.junit.rules.ExpectedException; import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -39,6 +40,18 @@ public void shouldGetStringBytes() throws Exception { assertTrue(Arrays.equals(expectedBytes, HMACAlgorithm.getSecretBytes(text))); } + @Test + public void shouldKeepCopyTheReceivedSecretArray() throws Exception { + String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + byte[] secretArray = "secret".getBytes(Charset.defaultCharset()); + Algorithm algorithmString = Algorithm.HMAC256(secretArray); + + DecodedJWT decoded = JWT.decode(jwt); + algorithmString.verify(decoded); + secretArray[0] = secretArray[1]; + algorithmString.verify(decoded); + } + @Test public void shouldPassHMAC256Verification() throws Exception { String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; From b161fc4dbf33dd8fb04541e898a7ae9e18b1f3a9 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:11:54 -0300 Subject: [PATCH 086/293] fix array manipulation by making a copy of the received value --- lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 5df23a83..1bdb2f5d 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -8,6 +8,7 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; class HMACAlgorithm extends Algorithm { @@ -20,7 +21,7 @@ class HMACAlgorithm extends Algorithm { if (secretBytes == null) { throw new IllegalArgumentException("The Secret cannot be null"); } - this.secret = secretBytes; + this.secret = Arrays.copyOf(secretBytes, secretBytes.length); this.crypto = crypto; } From 542f7fa543270f4434bc7747a13cc17bf9824ce3 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:19:16 -0300 Subject: [PATCH 087/293] use final modifier on a field param --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 0b4da54b..5fa1fe07 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -59,7 +59,7 @@ static JWTCreator.Builder init() { */ public static class Builder { private final Map payloadClaims; - private Map headerClaims; + private final Map headerClaims; Builder() { this.payloadClaims = new HashMap<>(); From 54286fccd3234cc1a458236d912e3c48f1f94164 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 18:42:07 -0300 Subject: [PATCH 088/293] document java thread-safety --- .../main/java/com/auth0/jwt/ClockImpl.java | 8 +++++ .../main/java/com/auth0/jwt/JWTCreator.java | 2 ++ .../main/java/com/auth0/jwt/JWTDecoder.java | 2 ++ .../main/java/com/auth0/jwt/JWTVerifier.java | 2 ++ .../com/auth0/jwt/algorithms/Algorithm.java | 29 ++++++++++--------- .../auth0/jwt/algorithms/CryptoHelper.java | 6 +++- .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 7 ++++- .../auth0/jwt/algorithms/HMACAlgorithm.java | 5 ++++ .../auth0/jwt/algorithms/RSAAlgorithm.java | 5 ++++ .../auth0/jwt/impl/HeaderDeserializer.java | 11 +++++-- .../auth0/jwt/impl/PayloadDeserializer.java | 11 +++++-- .../java/com/auth0/jwt/impl/PayloadImpl.java | 6 +++- .../com/auth0/jwt/impl/PayloadSerializer.java | 13 +++++++-- 13 files changed, 83 insertions(+), 24 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/ClockImpl.java b/lib/src/main/java/com/auth0/jwt/ClockImpl.java index 45e3edfc..29ed3c1e 100644 --- a/lib/src/main/java/com/auth0/jwt/ClockImpl.java +++ b/lib/src/main/java/com/auth0/jwt/ClockImpl.java @@ -4,6 +4,14 @@ import java.util.Date; +/** + * Default Clock implementation used for verification. + * + * @see Clock + * @see JWTVerifier + *

+ * This class is thread-safe. + */ final class ClockImpl implements Clock { ClockImpl() { diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 5fa1fe07..bd0dff9d 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -21,6 +21,8 @@ /** * The JWTCreator class holds the sign method to generate a complete JWT (with Signature) from a given Header and Payload content. + *

+ * This class is thread-safe. */ @SuppressWarnings("WeakerAccess") public final class JWTCreator { diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index b14c2ac3..ffd0217c 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -16,6 +16,8 @@ /** * The JWTDecoder class holds the decode method to parse a given JWT token into it's JWT representation. + *

+ * This class is thread-safe. */ @SuppressWarnings("WeakerAccess") final class JWTDecoder implements DecodedJWT, Serializable { diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index a5fab486..487addaf 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -13,6 +13,8 @@ /** * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches. + *

+ * This class is thread-safe. */ @SuppressWarnings("WeakerAccess") public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 24ad025b..1a8c6c2d 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -6,11 +6,12 @@ import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; -import java.io.ByteArrayOutputStream; import java.security.interfaces.*; /** * The Algorithm class represents an algorithm to be used in the Signing or Verification process of a Token. + *

+ * This class and its subclasses are thread-safe. */ @SuppressWarnings("WeakerAccess") public abstract class Algorithm { @@ -137,7 +138,7 @@ public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { * * @param secret the secret to use in the verify or signing instance. * @return a valid HMAC256 Algorithm. - * @throws IllegalArgumentException if the provided Secret is null. + * @throws IllegalArgumentException if the provided Secret is null. */ public static Algorithm HMAC256(String secret) throws IllegalArgumentException { return new HMACAlgorithm("HS256", "HmacSHA256", secret); @@ -148,7 +149,7 @@ public static Algorithm HMAC256(String secret) throws IllegalArgumentException { * * @param secret the secret to use in the verify or signing instance. * @return a valid HMAC384 Algorithm. - * @throws IllegalArgumentException if the provided Secret is null. + * @throws IllegalArgumentException if the provided Secret is null. */ public static Algorithm HMAC384(String secret) throws IllegalArgumentException { return new HMACAlgorithm("HS384", "HmacSHA384", secret); @@ -159,7 +160,7 @@ public static Algorithm HMAC384(String secret) throws IllegalArgumentException { * * @param secret the secret to use in the verify or signing instance. * @return a valid HMAC512 Algorithm. - * @throws IllegalArgumentException if the provided Secret is null. + * @throws IllegalArgumentException if the provided Secret is null. */ public static Algorithm HMAC512(String secret) throws IllegalArgumentException { return new HMACAlgorithm("HS512", "HmacSHA512", secret); @@ -365,20 +366,20 @@ public String toString() { /** * Sign the given content using this Algorithm instance. * - * @param headerBytes an array of bytes representing the base64 encoded header content to be verified against the signature. + * @param headerBytes an array of bytes representing the base64 encoded header content to be verified against the signature. * @param payloadBytes an array of bytes representing the base64 encoded payload content to be verified against the signature. * @return the signature in a base64 encoded array of bytes * @throws SignatureGenerationException if the Key is invalid. */ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { - // default implementation; keep around until sign(byte[]) method is removed - byte[] contentBytes = new byte[headerBytes.length + 1 + payloadBytes.length]; - - System.arraycopy(headerBytes, 0, contentBytes, 0, headerBytes.length); - contentBytes[headerBytes.length] = (byte)'.'; - System.arraycopy(payloadBytes, 0, contentBytes, headerBytes.length + 1, payloadBytes.length); - - return sign(contentBytes); + // default implementation; keep around until sign(byte[]) method is removed + byte[] contentBytes = new byte[headerBytes.length + 1 + payloadBytes.length]; + + System.arraycopy(headerBytes, 0, contentBytes, 0, headerBytes.length); + contentBytes[headerBytes.length] = (byte) '.'; + System.arraycopy(payloadBytes, 0, contentBytes, headerBytes.length + 1, payloadBytes.length); + + return sign(contentBytes); } /** @@ -389,7 +390,7 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene * @throws SignatureGenerationException if the Key is invalid. * @deprecated Please use the {@linkplain #sign(byte[], byte[])} method instead. */ - + @Deprecated public abstract byte[] sign(byte[] contentBytes) throws SignatureGenerationException; diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index dc92ff97..62aec632 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -2,10 +2,14 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; - import java.nio.charset.StandardCharsets; import java.security.*; +/** + * Class used to perform the signature hash calculations. + *

+ * This class is thread-safe. + */ class CryptoHelper { private static final byte JWT_PART_SEPARATOR = (byte)46; diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 12ddca70..6d065c9c 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -12,6 +12,11 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +/** + * Subclass representing an Elliptic Curve signing algorithm + *

+ * This class is thread-safe. + */ class ECDSAAlgorithm extends Algorithm { private final ECDSAKeyProvider keyProvider; @@ -65,7 +70,7 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene throw new SignatureGenerationException(this, e); } } - + @Override @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 1bdb2f5d..596f907e 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -10,6 +10,11 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; +/** + * Subclass representing an Hash-based MAC signing algorithm + *

+ * This class is thread-safe. + */ class HMACAlgorithm extends Algorithm { private final CryptoHelper crypto; diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index 15cce55a..a61fc97e 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -13,6 +13,11 @@ import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +/** + * Subclass representing an RSA signing algorithm + *

+ * This class is thread-safe. + */ class RSAAlgorithm extends Algorithm { private final RSAKeyProvider keyProvider; diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java index cea2944a..6bba2caa 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -11,17 +11,24 @@ import java.io.IOException; import java.util.Map; +/** + * Jackson deserializer implementation for converting from JWT Header parts. + * + * @see JWTParser + *

+ * This class is thread-safe. + */ class HeaderDeserializer extends StdDeserializer { private final ObjectReader objectReader; - + HeaderDeserializer(ObjectReader objectReader) { this(null, objectReader); } private HeaderDeserializer(Class vc, ObjectReader objectReader) { super(vc); - + this.objectReader = objectReader; } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 0c9de6c7..8935a277 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -13,17 +13,24 @@ import java.io.IOException; import java.util.*; +/** + * Jackson deserializer implementation for converting from JWT Payload parts. + * + * @see JWTParser + *

+ * This class is thread-safe. + */ class PayloadDeserializer extends StdDeserializer { private final ObjectReader objectReader; - + PayloadDeserializer(ObjectReader reader) { this(null, reader); } private PayloadDeserializer(Class vc, ObjectReader reader) { super(vc); - + this.objectReader = reader; } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index cf678f81..f056c038 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -11,7 +11,11 @@ import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim; /** - * The PayloadImpl class implements the Payload interface. + * Decoder of string JSON Web Tokens into their POJO representations. + * + * @see Payload + *

+ * This class is thread-safe. */ class PayloadImpl implements Payload, Serializable { diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index dd0d2f42..684137cc 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -8,6 +8,13 @@ import java.util.Date; import java.util.Map; +/** + * Jackson serializer implementation for converting into JWT Payload parts. + * + * @see com.auth0.jwt.JWTCreator + *

+ * This class is thread-safe. + */ public class PayloadSerializer extends StdSerializer { public PayloadSerializer() { @@ -20,14 +27,14 @@ private PayloadSerializer(Class t) { @Override public void serialize(ClaimsHolder holder, JsonGenerator gen, SerializerProvider provider) throws IOException { - + gen.writeStartObject(); for (Map.Entry e : holder.getClaims().entrySet()) { switch (e.getKey()) { case PublicClaims.AUDIENCE: if (e.getValue() instanceof String) { gen.writeFieldName(e.getKey()); - gen.writeString((String)e.getValue()); + gen.writeString((String) e.getValue()); break; } String[] audArray = (String[]) e.getValue(); @@ -37,7 +44,7 @@ public void serialize(ClaimsHolder holder, JsonGenerator gen, SerializerProvider } else if (audArray.length > 1) { gen.writeFieldName(e.getKey()); gen.writeStartArray(); - for(String aud : audArray) { + for (String aud : audArray) { gen.writeString(aud); } gen.writeEndArray(); From 9fdf105c65cded2a4fde33facf24db6f581e5c08 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 16 Jun 2020 13:46:04 -0300 Subject: [PATCH 089/293] Update lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java Co-authored-by: Rita Zerrizuela --- .../test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 22d29258..38aaf301 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -41,7 +41,7 @@ public void shouldGetStringBytes() throws Exception { } @Test - public void shouldKeepCopyTheReceivedSecretArray() throws Exception { + public void shouldCopyTheReceivedSecretArray() throws Exception { String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; byte[] secretArray = "secret".getBytes(Charset.defaultCharset()); Algorithm algorithmString = Algorithm.HMAC256(secretArray); @@ -293,4 +293,4 @@ public void shouldBeEqualSignatureMethodResults() throws Exception { assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); } -} \ No newline at end of file +} From 5dbe4e84eef0422720f504cb10a4ec03e87cf2e8 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 12 Jun 2020 16:39:50 -0300 Subject: [PATCH 090/293] wrap IllegalArgumentException into JWTDecodeException --- lib/src/main/java/com/auth0/jwt/JWTDecoder.java | 2 ++ lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index ffd0217c..445bf95d 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -41,6 +41,8 @@ final class JWTDecoder implements DecodedJWT, Serializable { payloadJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[1])); } catch (NullPointerException e) { throw new JWTDecodeException("The UTF-8 Charset isn't initialized.", e); + } catch (IllegalArgumentException e){ + throw new JWTDecodeException("The input is not a valid base 64 encoded string.", e); } header = converter.parseHeader(headerJson); payload = converter.parsePayload(payloadJson); diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index dfe59182..efd1b398 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -32,6 +32,13 @@ public void getSubject() throws Exception { } // Exceptions + @Test + public void shouldThrowIfTheContentIsNotProperlyEncoded() throws Exception { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The input is not a valid base 64 encoded string."); + JWT.decode("eyJ0eXAiOiJKV1QiLCJhbGciO-corrupted.eyJ0ZXN0IjoxMjN9.sLtFC2rLAzN0-UJ13OLQX6ezNptAQzespaOGwCnpqk"); + } + @Test public void shouldThrowIfLessThan3Parts() throws Exception { exception.expect(JWTDecodeException.class); From 984eaaf0fb74cba344d3f743aa0fa0d0724edcec Mon Sep 17 00:00:00 2001 From: fossabot Date: Wed, 9 Sep 2020 10:59:06 -0700 Subject: [PATCH 091/293] Add license scan report and status Signed off by: fossabot --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0685729..70d6f49c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ - - # Java JWT [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) [![Javadoc](https://javadoc.io/badge2/com.auth0/java-jwt/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt?ref=badge_shield) A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). @@ -433,3 +432,6 @@ If you have found a bug or if you have a feature request, please report them at ## License This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. + + +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt?ref=badge_large) \ No newline at end of file From 027ff9516d2864900810295c32710e5323a6f3fa Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 14 Sep 2020 17:27:25 -0500 Subject: [PATCH 092/293] PR 428 original changes --- README.md | 1 + lib/README-ES256K.txt | 21 ++++ .../com/auth0/jwt/algorithms/Algorithm.java | 45 ++++++- .../com/auth0/jwt/ConcurrentVerifyTest.java | 15 ++- .../java/com/auth0/jwt/JWTCreatorTest.java | 39 +++++- lib/src/test/java/com/auth0/jwt/JWTTest.java | 20 ++- lib/src/test/java/com/auth0/jwt/PemUtils.java | 8 +- .../auth0/jwt/algorithms/AlgorithmTest.java | 45 +++++++ .../jwt/algorithms/ECDSAAlgorithmTest.java | 114 ++++++++++++++++- .../ECDSABouncyCastleProviderTests.java | 116 +++++++++++++++++- lib/src/test/resources/ec256k-key-private.pem | 5 + .../resources/ec256k-key-public-invalid.pem | 4 + lib/src/test/resources/ec256k-key-public.pem | 4 + 13 files changed, 422 insertions(+), 15 deletions(-) create mode 100644 lib/README-ES256K.txt create mode 100644 lib/src/test/resources/ec256k-key-private.pem create mode 100644 lib/src/test/resources/ec256k-key-public-invalid.pem create mode 100644 lib/src/test/resources/ec256k-key-public.pem diff --git a/README.md b/README.md index 70d6f49c..1cdbe422 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ The library implements JWT Verification and Signing using the following algorith | RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 | | RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 | | ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 | +| ES256K | ECDSA256 | ECDSA with curve secp256k1 and SHA-256 | | ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | | ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | diff --git a/lib/README-ES256K.txt b/lib/README-ES256K.txt new file mode 100644 index 00000000..02fdf82e --- /dev/null +++ b/lib/README-ES256K.txt @@ -0,0 +1,21 @@ + +How to create a secp256k1 key to be used with ES256K algorithm: + +## first we generate the key +openssl ecparam -genkey -name secp256k1 -out KEY.pem -text +## the file is split into 2 parts a curve specification (EC PARAMETERS) and the key value +## this format is not suitable to be read by the PermUtils PEM reader so we have to convert it +## into PCKS8 format: + +openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in KEY.pem -out ec256k-key.private.pem + +## the public key is then calculated as follows: +openssl ec -pubout -in KEY.pem -out ec256k-key-public.pem + +## +## how to get DER signature +ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); +String[] parts = ES256K_JWT.split("\\."); + +byte[] derSignature = algorithm256.JOSEToDER(Base64.decodeBase64(parts[2])); +String jwt=parts[0]+"."+parts[1]+"."+Base64.encodeBase64URLSafeString(derSignature); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 1a8c6c2d..0460c287 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -176,7 +176,48 @@ public static Algorithm HMAC512(String secret) throws IllegalArgumentException { public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { return new HMACAlgorithm("HS256", "HmacSHA256", secret); } - + + /** + * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256K". + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid ECDSA256 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm ECDSA256K(ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + return new ECDSAAlgorithm("ES256K", "SHA256withECDSA", 32, keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256K". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid ECDSA256 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm ECDSA256K(ECPublicKey publicKey, ECPrivateKey privateKey) throws IllegalArgumentException { + return ECDSA256K(ECDSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + + /** + * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". + * + * @param key the key to use in the verify or signing instance. + * @return a valid ECDSA256 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + * @deprecated use {@link #ECDSA256(ECPublicKey, ECPrivateKey)} or {@link #ECDSA256(ECDSAKeyProvider)} + */ + @Deprecated + public static Algorithm ECDSA256K(ECKey key) throws IllegalArgumentException { + ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; + ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; + return ECDSA256K(publicKey, privateKey); + } + + + + /** * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * @@ -198,6 +239,8 @@ public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { public static Algorithm HMAC512(byte[] secret) throws IllegalArgumentException { return new HMACAlgorithm("HS512", "HmacSHA512", secret); } + + /** * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". diff --git a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java index 41098c25..a7f18f29 100644 --- a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java +++ b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java @@ -25,6 +25,7 @@ public class ConcurrentVerifyTest { private static final int REPEAT_COUNT = 1000; private static final String PUBLIC_KEY_FILE = "src/test/resources/rsa-public.pem"; private static final String PUBLIC_KEY_FILE_256 = "src/test/resources/ec256-key-public.pem"; + private static final String PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public.pem"; private static final String PUBLIC_KEY_FILE_384 = "src/test/resources/ec384-key-public.pem"; private static final String PUBLIC_KEY_FILE_512 = "src/test/resources/ec512-key-public.pem"; @@ -84,7 +85,7 @@ public void shouldPassHMAC256Verification() throws Exception { concurrentVerify(verifier, token); } - + @Test public void shouldPassHMAC384Verification() throws Exception { String token = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; @@ -139,7 +140,17 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { concurrentVerify(verifier, token); } - + + @Test + public void shouldPassECDSA256KVerificationWithJOSESignature() throws Exception { + String token = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJhdXRoMCJ9.W-AbsnuQ4vqmPftAyQuF09hn3oGn3tN7VGergxyMbK74yEzDV-mLyC3o3fxXrZxcW5h01DM6BckNag7ZcimPjw"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(key); + JWTVerifier verifier = JWTVerifier.init(algorithm).withIssuer("auth0").build(); + + concurrentVerify(verifier, token); + } + @Test public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { String token = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index c8dcae8d..1387425a 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -27,6 +27,7 @@ public class JWTCreatorTest { private static final String PRIVATE_KEY_FILE_RSA = "src/test/resources/rsa-private.pem"; private static final String PRIVATE_KEY_FILE_EC_256 = "src/test/resources/ec256-key-private.pem"; + private static final String PRIVATE_KEY_FILE_EC_256K = "src/test/resources/ec256k-key-private.pem"; @Rule public ExpectedException exception = ExpectedException.none(); @@ -157,7 +158,41 @@ public void shouldNotOverwriteKeyIdIfAddedFromRSAAlgorithms() throws Exception { String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } - + + @Test + public void shouldAddKeyIdIfAvailableFromECDSAKAlgorithms() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K + , "EC"); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .sign(Algorithm.ECDSA256K(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + + @Test + public void shouldNotOverwriteKeyIdIfAddedFromECDSAKAlgorithms() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .withKeyId("real-key-id") + .sign(Algorithm.ECDSA256(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + @Test public void shouldAddKeyIdIfAvailableFromECDSAAlgorithms() throws Exception { ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC"); @@ -683,4 +718,4 @@ public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception { .sign(Algorithm.HMAC256("secret")); } -} \ No newline at end of file +} diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 7cbe4b22..a51687e8 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -26,9 +26,11 @@ public class JWTTest { private static final String PRIVATE_KEY_FILE_RSA = "src/test/resources/rsa-private.pem"; private static final String PUBLIC_KEY_FILE_EC_256 = "src/test/resources/ec256-key-public.pem"; + private static final String PUBLIC_KEY_FILE_EC_256K = "src/test/resources/ec256k-key-public.pem"; private static final String PUBLIC_KEY_FILE_EC_384 = "src/test/resources/ec384-key-public.pem"; private static final String PUBLIC_KEY_FILE_EC_512 = "src/test/resources/ec512-key-public.pem"; private static final String PRIVATE_KEY_FILE_EC_256 = "src/test/resources/ec256-key-private.pem"; + private static final String PRIVATE_KEY_FILE_EC_256K = "src/test/resources/ec256k-key-private.pem"; private static final String PRIVATE_KEY_FILE_EC_384 = "src/test/resources/ec384-key-private.pem"; private static final String PRIVATE_KEY_FILE_EC_512 = "src/test/resources/ec512-key-private.pem"; @@ -494,7 +496,23 @@ public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { .build(); assertThat(verified, is(notNullValue())); } - + + @Test + public void shouldCreateAnEmptyECDSA256KSignedToken() throws Exception { + String signed = JWT.create().sign(Algorithm.ECDSA256K((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"))); + assertThat(signed, is(notNullValue())); + + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES256K")); + assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); + assertThat(parts[1], is("e30")); + + JWTVerifier verified = JWT.require(Algorithm.ECDSA256K((ECKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_256K, "EC"))) + .build(); + assertThat(verified, is(notNullValue())); + } + @Test public void shouldCreateAnEmptyECDSA384SignedToken() throws Exception { String signed = JWT.create().sign(Algorithm.ECDSA384((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_384, "EC"))); diff --git a/lib/src/test/java/com/auth0/jwt/PemUtils.java b/lib/src/test/java/com/auth0/jwt/PemUtils.java index 5f026b0a..6cd7703b 100644 --- a/lib/src/test/java/com/auth0/jwt/PemUtils.java +++ b/lib/src/test/java/com/auth0/jwt/PemUtils.java @@ -1,5 +1,6 @@ package com.auth0.jwt; +import org.bouncycastle.jce.provider.PEMUtil; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; @@ -11,10 +12,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.spec.EncodedKeySpec; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; +import java.security.spec.*; public class PemUtils { @@ -43,7 +41,7 @@ private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) { return publicKey; } - + private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) { PrivateKey privateKey = null; try { diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java index f4f437f9..8769a4f2 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -449,6 +449,51 @@ public void shouldCreateECDSA256AlgorithmWithProvider() throws Exception { assertThat(algorithm.getDescription(), is("SHA256withECDSA")); assertThat(algorithm.getName(), is("ES256")); } + + @Test + public void shouldCreateECDSA256KAlgorithmWithPublicKey() throws Exception { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); + Algorithm algorithm = Algorithm.ECDSA256K(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256K")); + } + + @Test + public void shouldCreateECDSA256KAlgorithmWithPrivateKey() throws Exception { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); + Algorithm algorithm = Algorithm.ECDSA256K(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256K")); + } + + @Test + public void shouldCreateECDSA256KAlgorithmWithBothKeys() throws Exception { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + Algorithm algorithm = Algorithm.ECDSA256K(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256K")); + } + + @Test + public void shouldCreateECDSA256KAlgorithmWithProvider() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + Algorithm algorithm = Algorithm.ECDSA256K(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256K")); + } @Test public void shouldCreateECDSA384AlgorithmWithPublicKey() throws Exception { diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index b50832d1..7f7b9c94 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -40,7 +40,12 @@ public class ECDSAAlgorithmTest { private static final String PRIVATE_KEY_FILE_256 = "src/test/resources/ec256-key-private.pem"; private static final String PUBLIC_KEY_FILE_256 = "src/test/resources/ec256-key-public.pem"; private static final String INVALID_PUBLIC_KEY_FILE_256 = "src/test/resources/ec256-key-public-invalid.pem"; - + + private static final String PRIVATE_KEY_FILE_256K = "src/test/resources/ec256k-key-private.pem"; + private static final String PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public.pem"; + private static final String INVALID_PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public-invalid.pem"; + private static final String ES256K_JWT = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.e30.2ggPsc4xwQhYcgJueo3uQ14MpaVJ3AbEE8UE-wA9fc8SMibeW54gjZbikL-JBHqhEwc22Cp8DNOtadXsM81RGQ"; + private static final String PRIVATE_KEY_FILE_384 = "src/test/resources/ec384-key-private.pem"; private static final String PUBLIC_KEY_FILE_384 = "src/test/resources/ec384-key-public.pem"; private static final String INVALID_PUBLIC_KEY_FILE_384 = "src/test/resources/ec384-key-public-invalid.pem"; @@ -183,7 +188,112 @@ public void shouldFailECDSA256VerificationOnInvalidDERSignature() throws Excepti Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); } - + + @Test + public void shouldPassECDSA256KVerificationWithJOSESignature() throws Exception { + ECPublicKey key = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(key, null); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldThrowOnECDSA256KVerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.e30.MEUCIQDaCA-xzjHBCFhyAm56je5DXgylpUncBsQTxQT7AD19zwIgEjIm3lueII2W4pC_iQR6oRMHNtgqfAzTrWnV7DPNURk"; + + ECPublicKey key = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(key, null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA256KVerificationWithJOSESignatureWithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC") + , (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldPassECDSA256KVerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA256K(provider); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWhenProvidedPublicKeyIsNull() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA256K(provider); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWhenUsingPrivateKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + Algorithm algorithm = Algorithm.ECDSA256K(null, (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = ES256K_JWT.substring(0,ES256K_JWT.length()-1); + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidJOSESignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + new SecureRandom().nextBytes(bytes); + String signature = Base64.encodeBase64URLSafeString(bytes); + String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + bytes[0] = 0x30; + new SecureRandom().nextBytes(bytes); + String signature = Base64.encodeBase64URLSafeString(bytes); + String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } @Test public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java index 452c0074..dbecae1e 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -32,6 +32,11 @@ import static org.mockito.Mockito.when; public class ECDSABouncyCastleProviderTests { + + + private static final String PRIVATE_KEY_FILE_256K = "src/test/resources/ec256k-key-private.pem"; + private static final String PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public.pem"; + private static final String INVALID_PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public-invalid.pem"; private static final String PRIVATE_KEY_FILE_256 = "src/test/resources/ec256-key-private.pem"; private static final String PUBLIC_KEY_FILE_256 = "src/test/resources/ec256-key-public.pem"; @@ -44,7 +49,9 @@ public class ECDSABouncyCastleProviderTests { private static final String PRIVATE_KEY_FILE_512 = "src/test/resources/ec512-key-private.pem"; private static final String PUBLIC_KEY_FILE_512 = "src/test/resources/ec512-key-public.pem"; private static final String INVALID_PUBLIC_KEY_FILE_512 = "src/test/resources/ec512-key-public-invalid.pem"; - + + private static final String ES256K_JWT = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.e30.2ggPsc4xwQhYcgJueo3uQ14MpaVJ3AbEE8UE-wA9fc8SMibeW54gjZbikL-JBHqhEwc22Cp8DNOtadXsM81RGQ"; + @Rule public ExpectedException exception = ExpectedException.none(); private static final Provider bcProvider = new BouncyCastleProvider(); @@ -72,7 +79,112 @@ public void shouldPreferBouncyCastleProvider() throws Exception { } // Verify - + @Test + public void shouldPassECDSA256KVerificationWithJOSESignature() throws Exception { + ECPublicKey key = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(key, null); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldThrowOnECDSA256KVerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.e30.MEUCIQDaCA-xzjHBCFhyAm56je5DXgylpUncBsQTxQT7AD19zwIgEjIm3lueII2W4pC_iQR6oRMHNtgqfAzTrWnV7DPNURk"; + + ECPublicKey key = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(key, null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA256KVerificationWithJOSESignatureWithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC") + , (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldPassECDSA256KVerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA256K(provider); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWhenProvidedPublicKeyIsNull() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA256K(provider); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationWhenUsingPrivateKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + Algorithm algorithm = Algorithm.ECDSA256K(null, (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); + algorithm.verify(JWT.decode(ES256K_JWT)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = ES256K_JWT.substring(0,ES256K_JWT.length()-1); + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidJOSESignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + new SecureRandom().nextBytes(bytes); + String signature = Base64.encodeBase64URLSafeString(bytes); + String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256KVerificationOnInvalidDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + bytes[0] = 0x30; + new SecureRandom().nextBytes(bytes); + String signature = Base64.encodeBase64URLSafeString(bytes); + String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; + Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); + algorithm.verify(JWT.decode(jwt)); + } + @Test public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; diff --git a/lib/src/test/resources/ec256k-key-private.pem b/lib/src/test/resources/ec256k-key-private.pem new file mode 100644 index 00000000..43038c8e --- /dev/null +++ b/lib/src/test/resources/ec256k-key-private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgN78SHuwC8BVLUbuEBXC9 +sn9LuX9a7yqIt1xaiPupqe6hRANCAAS7s8bnSsGqrv7YBqxqJiQKiyPJvcHTe2UG +7FpOaAlqIUWL5xbpCZbcH9yqozS55KMVxG78KskeuET7hl01J6EU +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/lib/src/test/resources/ec256k-key-public-invalid.pem b/lib/src/test/resources/ec256k-key-public-invalid.pem new file mode 100644 index 00000000..8809e21b --- /dev/null +++ b/lib/src/test/resources/ec256k-key-public-invalid.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEXzvxXn69y0I54+Yr0pMXqGyCb1R8D4dS +xaYhaRuN2fsV+unbFWUYViRoYUgMAbmvnXzVmThLUeP/PTmlv9lm7Q== +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/lib/src/test/resources/ec256k-key-public.pem b/lib/src/test/resources/ec256k-key-public.pem new file mode 100644 index 00000000..1513ed70 --- /dev/null +++ b/lib/src/test/resources/ec256k-key-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEu7PG50rBqq7+2AasaiYkCosjyb3B03tl +BuxaTmgJaiFFi+cW6QmW3B/cqqM0ueSjFcRu/CrJHrhE+4ZdNSehFA== +-----END PUBLIC KEY----- \ No newline at end of file From 10c443768cee70e693bb81c847c4a35af6a363f3 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 14 Sep 2020 17:35:39 -0500 Subject: [PATCH 093/293] Review feedback - remove deprecated method, whitespace fixes --- .../com/auth0/jwt/algorithms/Algorithm.java | 24 +++---------------- .../com/auth0/jwt/ConcurrentVerifyTest.java | 9 +++++-- lib/src/test/java/com/auth0/jwt/JWTTest.java | 9 +++++-- .../auth0/jwt/algorithms/AlgorithmTest.java | 24 +------------------ 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 0460c287..d6a3a2f4 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -176,7 +176,7 @@ public static Algorithm HMAC512(String secret) throws IllegalArgumentException { public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { return new HMACAlgorithm("HS256", "HmacSHA256", secret); } - + /** * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256K". * @@ -187,7 +187,7 @@ public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { public static Algorithm ECDSA256K(ECDSAKeyProvider keyProvider) throws IllegalArgumentException { return new ECDSAAlgorithm("ES256K", "SHA256withECDSA", 32, keyProvider); } - + /** * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256K". * @@ -199,25 +199,7 @@ public static Algorithm ECDSA256K(ECDSAKeyProvider keyProvider) throws IllegalAr public static Algorithm ECDSA256K(ECPublicKey publicKey, ECPrivateKey privateKey) throws IllegalArgumentException { return ECDSA256K(ECDSAAlgorithm.providerForKeys(publicKey, privateKey)); } - - /** - * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". - * - * @param key the key to use in the verify or signing instance. - * @return a valid ECDSA256 Algorithm. - * @throws IllegalArgumentException if the provided Key is null. - * @deprecated use {@link #ECDSA256(ECPublicKey, ECPrivateKey)} or {@link #ECDSA256(ECDSAKeyProvider)} - */ - @Deprecated - public static Algorithm ECDSA256K(ECKey key) throws IllegalArgumentException { - ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; - ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; - return ECDSA256K(publicKey, privateKey); - } - - - - + /** * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * diff --git a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java index a7f18f29..f4de1f71 100644 --- a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java +++ b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java @@ -10,11 +10,14 @@ import org.junit.rules.ExpectedException; import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; import java.util.Collections; import java.util.List; import java.util.concurrent.*; +import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; //@Ignore("Skipping concurrency tests") @@ -26,6 +29,7 @@ public class ConcurrentVerifyTest { private static final String PUBLIC_KEY_FILE = "src/test/resources/rsa-public.pem"; private static final String PUBLIC_KEY_FILE_256 = "src/test/resources/ec256-key-public.pem"; private static final String PUBLIC_KEY_FILE_256K = "src/test/resources/ec256k-key-public.pem"; + private static final String PRIVATE_KEY_FILE_256K = "src/test/resources/ec256k-key-private.pem"; private static final String PUBLIC_KEY_FILE_384 = "src/test/resources/ec384-key-public.pem"; private static final String PUBLIC_KEY_FILE_512 = "src/test/resources/ec512-key-public.pem"; @@ -144,8 +148,9 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { @Test public void shouldPassECDSA256KVerificationWithJOSESignature() throws Exception { String token = "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJhdXRoMCJ9.W-AbsnuQ4vqmPftAyQuF09hn3oGn3tN7VGergxyMbK74yEzDV-mLyC3o3fxXrZxcW5h01DM6BckNag7ZcimPjw"; - ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); - Algorithm algorithm = Algorithm.ECDSA256K(key); + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC"); + Algorithm algorithm = Algorithm.ECDSA256K(publicKey, privateKey); JWTVerifier verifier = JWTVerifier.init(algorithm).withIssuer("auth0").build(); concurrentVerify(verifier, token); diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index a51687e8..79eb1ea8 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -12,6 +12,8 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; import java.util.Date; @@ -499,7 +501,10 @@ public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { @Test public void shouldCreateAnEmptyECDSA256KSignedToken() throws Exception { - String signed = JWT.create().sign(Algorithm.ECDSA256K((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"))); + ECPublicKey publicKey = (ECPublicKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_256K, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"); + + String signed = JWT.create().sign(Algorithm.ECDSA256K(publicKey, privateKey) ); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); @@ -508,7 +513,7 @@ public void shouldCreateAnEmptyECDSA256KSignedToken() throws Exception { assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); - JWTVerifier verified = JWT.require(Algorithm.ECDSA256K((ECKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_256K, "EC"))) + JWTVerifier verified = JWT.require(Algorithm.ECDSA256K(publicKey, privateKey)) .build(); assertThat(verified, is(notNullValue())); } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java index 8769a4f2..50430017 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -449,29 +449,7 @@ public void shouldCreateECDSA256AlgorithmWithProvider() throws Exception { assertThat(algorithm.getDescription(), is("SHA256withECDSA")); assertThat(algorithm.getName(), is("ES256")); } - - @Test - public void shouldCreateECDSA256KAlgorithmWithPublicKey() throws Exception { - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); - Algorithm algorithm = Algorithm.ECDSA256K(key); - - assertThat(algorithm, is(notNullValue())); - assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); - assertThat(algorithm.getDescription(), is("SHA256withECDSA")); - assertThat(algorithm.getName(), is("ES256K")); - } - - @Test - public void shouldCreateECDSA256KAlgorithmWithPrivateKey() throws Exception { - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); - Algorithm algorithm = Algorithm.ECDSA256K(key); - - assertThat(algorithm, is(notNullValue())); - assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); - assertThat(algorithm.getDescription(), is("SHA256withECDSA")); - assertThat(algorithm.getName(), is("ES256K")); - } - + @Test public void shouldCreateECDSA256KAlgorithmWithBothKeys() throws Exception { ECPublicKey publicKey = mock(ECPublicKey.class); From 938f1a5e973eb2cc43902150888bc4cba7d4f808 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 14 Sep 2020 17:36:17 -0500 Subject: [PATCH 094/293] Remove README-ES256K.txt file --- lib/README-ES256K.txt | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 lib/README-ES256K.txt diff --git a/lib/README-ES256K.txt b/lib/README-ES256K.txt deleted file mode 100644 index 02fdf82e..00000000 --- a/lib/README-ES256K.txt +++ /dev/null @@ -1,21 +0,0 @@ - -How to create a secp256k1 key to be used with ES256K algorithm: - -## first we generate the key -openssl ecparam -genkey -name secp256k1 -out KEY.pem -text -## the file is split into 2 parts a curve specification (EC PARAMETERS) and the key value -## this format is not suitable to be read by the PermUtils PEM reader so we have to convert it -## into PCKS8 format: - -openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in KEY.pem -out ec256k-key.private.pem - -## the public key is then calculated as follows: -openssl ec -pubout -in KEY.pem -out ec256k-key-public.pem - -## -## how to get DER signature -ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256K, "EC")); -String[] parts = ES256K_JWT.split("\\."); - -byte[] derSignature = algorithm256.JOSEToDER(Base64.decodeBase64(parts[2])); -String jwt=parts[0]+"."+parts[1]+"."+Base64.encodeBase64URLSafeString(derSignature); From 4a55c3066ad0f5c4c5b14d55ea58a19c4bf8b4a4 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 16 Sep 2020 09:18:46 -0500 Subject: [PATCH 095/293] review feedback - whitespace and optimized imports --- .../test/java/com/auth0/jwt/JWTCreatorTest.java | 17 ++++++++--------- lib/src/test/java/com/auth0/jwt/JWTTest.java | 14 +++++++------- lib/src/test/java/com/auth0/jwt/PemUtils.java | 6 ++++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 1387425a..2d9179ff 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -158,41 +158,40 @@ public void shouldNotOverwriteKeyIdIfAddedFromRSAAlgorithms() throws Exception { String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } - + @Test public void shouldAddKeyIdIfAvailableFromECDSAKAlgorithms() throws Exception { - ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K - , "EC"); + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"); ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKeyId()).thenReturn("my-key-id"); when(provider.getPrivateKey()).thenReturn(privateKey); - + String signed = JWTCreator.init() .sign(Algorithm.ECDSA256K(provider)); - + assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } - + @Test public void shouldNotOverwriteKeyIdIfAddedFromECDSAKAlgorithms() throws Exception { ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"); ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKeyId()).thenReturn("my-key-id"); when(provider.getPrivateKey()).thenReturn(privateKey); - + String signed = JWTCreator.init() .withKeyId("real-key-id") .sign(Algorithm.ECDSA256(provider)); - + assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } - + @Test public void shouldAddKeyIdIfAvailableFromECDSAAlgorithms() throws Exception { ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC"); diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 79eb1ea8..b778be13 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -86,8 +86,8 @@ public void shouldVerifyDecodedToken() throws Exception { DecodedJWT decodedJWT = JWT.decode(token); RSAKey key = (RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"); DecodedJWT jwt = JWT.require(Algorithm.RSA512(key)) - .build() - .verify(decodedJWT); + .build() + .verify(decodedJWT); assertThat(jwt, is(notNullValue())); } @@ -498,26 +498,26 @@ public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { .build(); assertThat(verified, is(notNullValue())); } - + @Test public void shouldCreateAnEmptyECDSA256KSignedToken() throws Exception { ECPublicKey publicKey = (ECPublicKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_256K, "EC"); ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256K, "EC"); - String signed = JWT.create().sign(Algorithm.ECDSA256K(publicKey, privateKey) ); + String signed = JWT.create().sign(Algorithm.ECDSA256K(publicKey, privateKey)); assertThat(signed, is(notNullValue())); - + String[] parts = signed.split("\\."); String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES256K")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); - + JWTVerifier verified = JWT.require(Algorithm.ECDSA256K(publicKey, privateKey)) .build(); assertThat(verified, is(notNullValue())); } - + @Test public void shouldCreateAnEmptyECDSA384SignedToken() throws Exception { String signed = JWT.create().sign(Algorithm.ECDSA384((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_384, "EC"))); diff --git a/lib/src/test/java/com/auth0/jwt/PemUtils.java b/lib/src/test/java/com/auth0/jwt/PemUtils.java index 6cd7703b..6c92e05e 100644 --- a/lib/src/test/java/com/auth0/jwt/PemUtils.java +++ b/lib/src/test/java/com/auth0/jwt/PemUtils.java @@ -1,6 +1,5 @@ package com.auth0.jwt; -import org.bouncycastle.jce.provider.PEMUtil; import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; @@ -12,7 +11,10 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.spec.*; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; public class PemUtils { From 006c073e87d83a8da492882b7a16d7299e1cc4c7 Mon Sep 17 00:00:00 2001 From: Javier Salinas Date: Thu, 17 Sep 2020 18:25:50 +0200 Subject: [PATCH 096/293] Add gh-action to validate gradle wrapper --- .github/workflows/gradle-wrapper-validation.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/workflows/gradle-wrapper-validation.yml diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000..a015578a --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,10 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +jobs: + validation: + name: "validation/gradlew" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 From ac92da2659cfeea766aa404aa7d8d38ff26a400f Mon Sep 17 00:00:00 2001 From: Javier Salinas Date: Thu, 17 Sep 2020 18:25:56 +0200 Subject: [PATCH 097/293] Upgrade Gradle to 6.6.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58695 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 35 ++++++++++------------- gradlew.bat | 2 +- 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..f3d88b1c2faf2fc91d853cd5d4242b5547257070 100644 GIT binary patch delta 22808 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 delta 20005 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 nvEpR}Ffj;rE}sKLhd|1%F5dI!=ox-rLy2 Date: Thu, 24 Sep 2020 15:17:58 -0500 Subject: [PATCH 098/293] Add ability to verify claim presence (#442) * add ability to verify claim presence only * remove commented-out method * Use singleton class to mark a claim should have only presence checked * minor refactoring to claim verification logic for readability * add test for null claim name --- .../main/java/com/auth0/jwt/JWTVerifier.java | 88 +++++++---- .../auth0/jwt/interfaces/Verification.java | 8 + .../java/com/auth0/jwt/JWTVerifierTest.java | 141 ++++++++++++++++++ 3 files changed, 212 insertions(+), 25 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 487addaf..a311ab33 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -3,6 +3,7 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; import com.auth0.jwt.impl.JWTParser; +import com.auth0.jwt.impl.NullClaim; import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Clock; @@ -115,6 +116,13 @@ public Verification withJWTId(String jwtId) { return this; } + @Override + public Verification withClaimPresence(String name) throws IllegalArgumentException { + assertNonNull(name); + requireClaim(name, NonEmptyClaim.getInstance()); + return this; + } + @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); @@ -289,35 +297,49 @@ private void verifyAlgorithm(DecodedJWT jwt, Algorithm expectedAlgorithm) throws private void verifyClaims(DecodedJWT jwt, Map claims) throws TokenExpiredException, InvalidClaimException { for (Map.Entry entry : claims.entrySet()) { - switch (entry.getKey()) { - case PublicClaims.AUDIENCE: - assertValidAudienceClaim(jwt.getAudience(), (List) entry.getValue()); - break; - case PublicClaims.EXPIRES_AT: - assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true); - break; - case PublicClaims.ISSUED_AT: - assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false); - break; - case PublicClaims.NOT_BEFORE: - assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false); - break; - case PublicClaims.ISSUER: - assertValidIssuerClaim(jwt.getIssuer(), (List) entry.getValue()); - break; - case PublicClaims.JWT_ID: - assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue()); - break; - case PublicClaims.SUBJECT: - assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue()); - break; - default: - assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue()); - break; + if (entry.getValue() instanceof NonEmptyClaim) { + assertClaimPresent(jwt.getClaim(entry.getKey()), entry.getKey()); + } else { + verifyClaimValues(jwt, entry); } } } + private void verifyClaimValues(DecodedJWT jwt, Map.Entry entry) { + switch (entry.getKey()) { + case PublicClaims.AUDIENCE: + assertValidAudienceClaim(jwt.getAudience(), (List) entry.getValue()); + break; + case PublicClaims.EXPIRES_AT: + assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true); + break; + case PublicClaims.ISSUED_AT: + assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false); + break; + case PublicClaims.NOT_BEFORE: + assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false); + break; + case PublicClaims.ISSUER: + assertValidIssuerClaim(jwt.getIssuer(), (List) entry.getValue()); + break; + case PublicClaims.JWT_ID: + assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue()); + break; + case PublicClaims.SUBJECT: + assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue()); + break; + default: + assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue()); + break; + } + } + + private void assertClaimPresent(Claim claim, String claimName) { + if (claim instanceof NullClaim) { + throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", claimName)); + } + } + private void assertValidClaim(Claim claim, String claimName, Object value) { boolean isValid = false; if (value instanceof String) { @@ -400,4 +422,20 @@ private void assertValidIssuerClaim(String issuer, List value) { throw new InvalidClaimException("The Claim 'iss' value doesn't match the required issuer."); } } + + /** + * Simple singleton used to mark that a claim should only be verified for presence. + */ + private static class NonEmptyClaim { + private static NonEmptyClaim nonEmptyClaim; + + private NonEmptyClaim() {} + + public static NonEmptyClaim getInstance() { + if (nonEmptyClaim == null) { + nonEmptyClaim = new NonEmptyClaim(); + } + return nonEmptyClaim; + } + } } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 72ae35d2..e23f4340 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -80,6 +80,14 @@ public interface Verification { */ Verification withJWTId(String jwtId); + /** + * Require a claim to be present, with any value. + * @param name the Claim's name. + * @return this same Verification instance + * @throws IllegalArgumentException if the name is null. + */ + Verification withClaimPresence(String name) throws IllegalArgumentException; + /** * Require a specific Claim value. * diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index d57ca187..156a39a6 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -790,4 +790,145 @@ public void shouldSkipClaimValidationsIfNoClaimsRequired() throws Exception { assertThat(jwt, is(notNullValue())); } + + @Test + public void shouldThrowWhenVerifyingClaimPresenceButClaimNotPresent() { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'missing' is not present in the JWT."); + + String jwt = JWTCreator.init() + .withClaim("custom", "") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("missing") + .build(); + + verifier.verify(jwt); + } + + @Test + public void shouldThrowWhenVerifyingClaimPresenceWhenClaimNameIsNull() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Custom Claim's name can't be null."); + + String jwt = JWTCreator.init() + .withClaim("custom", "value") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence(null); + } + + @Test + public void shouldVerifyStringClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", "") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyBooleanClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", true) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyIntegerClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", 123) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyLongClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", 922337203685477600L) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyDoubleClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", 12.34) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyListClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", Collections.singletonList("item")) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyMapClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("custom", Collections.singletonMap("key", "value")) + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("custom") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldVerifyStandardClaimPresence() { + String jwt = JWTCreator.init() + .withClaim("aud", "any value") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("aud") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } } From c5b785aca15c5c3265314f4f4d5fa7263594c205 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 25 Sep 2020 13:54:23 -0500 Subject: [PATCH 099/293] Release 3.11.0 --- CHANGELOG.md | 11 +++++++++++ README.md | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21095ff7..39da33e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [3.11.0](https://github.com/auth0/java-jwt/tree/3.11.0) (2020-09-25) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.3...3.11.0) + +**Added** +- Add ability to verify claim presence [\#442](https://github.com/auth0/java-jwt/pull/442) ([jimmyjames](https://github.com/jimmyjames)) +- Add Support for secp256k1 algorithms (AKA ES256K) [\#439](https://github.com/auth0/java-jwt/pull/439) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Fix and document thread-safety [\#427](https://github.com/auth0/java-jwt/pull/427) ([lbalmaceda](https://github.com/lbalmaceda)) +- Wrap IllegalArgumentException into JWTDecodeException [\#426](https://github.com/auth0/java-jwt/pull/426) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [3.10.3](https://github.com/auth0/java-jwt/tree/3.10.3) (2020-04-24) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.2...3.10.3) diff --git a/README.md b/README.md index 1cdbe422..d1b5948c 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.10.3 + 3.11.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.10.3' +implementation 'com.auth0:java-jwt:3.11.0' ``` ## Available Algorithms From dd6bc25d9a7c2857bae29757a9198b798ac5734f Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 27 Oct 2020 08:54:20 -0300 Subject: [PATCH 100/293] Setup the CODEOWNERS for pull request reviews (#448) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9ff4921..60f116c0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdks-approver +* @auth0/dx-sdks-engineer From a26284a3228e867e47dc96c2a31b92ee8dda0d83 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 28 Oct 2020 13:49:09 -0500 Subject: [PATCH 101/293] update issue templates --- .github/ISSUE_TEMPLATE.md | 38 ---------------- .github/ISSUE_TEMPLATE/config.yml | 8 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 39 ++++++++++++++++ .github/ISSUE_TEMPLATE/report-a-bug.md | 55 +++++++++++++++++++++++ 4 files changed, 102 insertions(+), 38 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/report-a-bug.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 009ccd24..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,38 +0,0 @@ -In order to efficiently and accurately address your issue or feature request, please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. Please delete any sections or questions below that do not pertain to this request. - -For general support or usage questions, please use the [Auth0 Community](https://community.auth0.com/) or [Auth0 Support](https://support.auth0.com/). - -### Description - -Description of the bug or feature request and why it's a problem. Consider including: - -- The use case or overall problem you're trying to solve -- Information about when the problem started - -### Prerequisites - -- [ ] I have checked the documentation for this library in the README. -- [ ] I have checked the [Auth0 Community](https://community.auth0.com/) for related posts. -- [ ] I have checked for related or duplicate [Issues](https://github.com/auth0/java-jwt/issues) and [PRs](https://github.com/auth0/java-jwt/pulls). -- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md). -- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). -- [ ] I am reporting this to the correct repository (note this library is used by many libraries, such as [auth0-java](https://github.com/auth0/auth0-java)). - -### Environment - -Please provide the following: - -- Version of this library used: -- Version of Java framework used: -- Additional libraries that might be affecting your instance: - -### Reproduction - -Detail the steps taken to reproduce this error and note if this issue can be reproduced consistently or if it is intermittent. - -Please include: - -- Code sample to reproduce the issue -- Log files (redact/remove sensitive information) -- Application settings (redact/remove sensitive information) -- Screenshots, if helpful diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..77c79de9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Auth0 Community + url: https://community.auth0.com/c/sdks + about: Discuss this SDK in the Auth0 Community forums + - name: Library Documentation + url: https://github.com/auth0/java-jwt/blob/master/README.md + about: Read the library documentation diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..5e9e90bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,39 @@ +--- +name: Feature request +about: Suggest an idea or a feature for this project +title: '' +labels: feature request +assignees: '' +--- + + + +### Describe the problem you'd like to have solved + + + +### Describe the ideal solution + + + +## Alternatives and current work-arounds + + + +### Additional information, if any + + diff --git a/.github/ISSUE_TEMPLATE/report-a-bug.md b/.github/ISSUE_TEMPLATE/report-a-bug.md new file mode 100644 index 00000000..60505a3f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report-a-bug.md @@ -0,0 +1,55 @@ +--- +name: Report a bug +about: Have you found a bug or issue? Create a bug report for this SDK +title: '' +labels: bug report +assignees: '' +--- + + + +### Describe the problem + + + +### What was the expected behavior? + + + +### Reproduction + + +- Step 1.. +- Step 2.. +- ... + +### Environment + + + +- **Version of this library used:** +- **Version of Java used:** +- **Other modules/plugins/libraries that might be involved:** +- **Any other relevant information you think would be useful:** From 0dba806b221187b064e72c9f4a204c0140aecfcc Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Fri, 30 Oct 2020 09:35:40 -0300 Subject: [PATCH 102/293] Revert "Add license scan report and status" This reverts commit 984eaaf0fb74cba344d3f743aa0fa0d0724edcec. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d1b5948c..a6cab21c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ + + # Java JWT [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) [![Javadoc](https://javadoc.io/badge2/com.auth0/java-jwt/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt?ref=badge_shield) A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). @@ -433,6 +434,3 @@ If you have found a bug or if you have a feature request, please report them at ## License This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. - - -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.amrom.workers.dev%2Fauth0%2Fjava-jwt?ref=badge_large) \ No newline at end of file From 2ba767459fd01f870fbf3375550f3a2c1147dc76 Mon Sep 17 00:00:00 2001 From: TeeVenDick Date: Tue, 3 Nov 2020 16:55:28 +0800 Subject: [PATCH 103/293] Fix broken docs links in README.md (#453) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a6cab21c..eefe1ccd 100644 --- a/README.md +++ b/README.md @@ -411,12 +411,12 @@ If the values can't be converted to the given **Class Type** a `JWTDecodeExcepti Auth0 helps you to: -* Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, among others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. -* Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. -* Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. -* Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. +* Add authentication with [multiple authentication sources](https://auth0.com/docs/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, among others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. +* Add authentication through more traditional **[username/password databases](https://auth0.com/docs/connections/database)**. +* Add support for **[linking different user accounts](https://auth0.com/docs/users/user-account-linking)** with the same user. +* Support for generating signed [Json Web Tokens](https://auth0.com/docs/tokens/json-web-tokens) to call your APIs and **flow the user identity** securely. * Analytics of how, when and where users are logging in. -* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). +* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://auth0.com/docs/rules). ## Create a free account in Auth0 From 4471c5960e8a0f94e2d34181b4654d8f9bbfe681 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Wed, 18 Nov 2020 11:06:21 -0300 Subject: [PATCH 104/293] Setup pull-request and issue templates (#458) * Setup pull-request and issue templates * Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 4 +- .github/ISSUE_TEMPLATE/report_a_bug.md | 55 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/report_a_bug.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 77c79de9..3cd7aa53 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Auth0 Community - url: https://community.auth0.com/c/sdks + url: https://community.auth0.com/c/sdks/5 about: Discuss this SDK in the Auth0 Community forums - name: Library Documentation url: https://github.com/auth0/java-jwt/blob/master/README.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 5e9e90bf..68352ba2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -17,7 +17,7 @@ By submitting an Issue to this repository, you agree to the terms within the Aut ### Describe the problem you'd like to have solved ### Describe the ideal solution @@ -36,4 +36,4 @@ By submitting an Issue to this repository, you agree to the terms within the Aut +--> \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md new file mode 100644 index 00000000..50b9fa7e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report_a_bug.md @@ -0,0 +1,55 @@ +--- +name: Report a bug +about: Have you found a bug or issue? Create a bug report for this SDK +title: '' +labels: bug report +assignees: '' +--- + + + +### Describe the problem + + + +### What was the expected behavior? + + + +### Reproduction + + +- Step 1.. +- Step 2.. +- ... + +### Environment + + + +- **Version of this library used:** +- **Which framework are you using, if applicable:** +- **Other modules/plugins/libraries that might be involved:** +- **Any other relevant information you think would be useful:** \ No newline at end of file From 45775cb8071ce2d1f48491c510991fdff27d9829 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 18 Nov 2020 17:21:42 -0600 Subject: [PATCH 105/293] Add notice to README about Java 8 upcoming requirement --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index eefe1ccd..712cb07d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.or If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. +> This library currently supports Java 7. Beginning soon, it will require Java 8 as the minimum supported Java version. See [this issue](https://github.com/auth0/java-jwt/issues/457) for additional information and timelines. + ## Installation The library is available on both Maven Central and Bintray, and the Javadoc is published [here](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html). From a787e2d79f890429fd7b7dc802f3f874db266bc2 Mon Sep 17 00:00:00 2001 From: LeeHainie <30332409+LeeHainie@users.noreply.github.com> Date: Mon, 23 Nov 2020 17:22:49 +0800 Subject: [PATCH 106/293] Thread-safe classes should be Shared statically Thread-safe classes should be Shared statically,This improves performance and reduces class creation costs. --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index bd0dff9d..340528ad 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -30,15 +30,21 @@ public final class JWTCreator { private final Algorithm algorithm; private final String headerJson; private final String payloadJson; + + private static final ObjectMapper mapper; + private static final SimpleModule module; + + static { + mapper = new ObjectMapper(); + module = new SimpleModule(); + module.addSerializer(ClaimsHolder.class, new PayloadSerializer()); + mapper.registerModule(module); + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + } private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) throws JWTCreationException { this.algorithm = algorithm; try { - ObjectMapper mapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(ClaimsHolder.class, new PayloadSerializer()); - mapper.registerModule(module); - mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); headerJson = mapper.writeValueAsString(headerClaims); payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims)); } catch (JsonProcessingException e) { From 0f9ee305334c6513e58014b113871a531d5738c9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Dec 2020 18:10:11 +0100 Subject: [PATCH 107/293] Update jackson-databind to 2.10.5.1 (fixes CVE-2020-25649) --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index dcd0d615..9ea22404 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -34,7 +34,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.3' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.5.1' implementation 'commons-codec:commons-codec:1.14' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From f616badb0a30963822d3fee8a789c133c73db547 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 15 Dec 2020 21:30:11 +0100 Subject: [PATCH 108/293] Target Java 8 (#455) * update gradle wrapper to 6.7 * change required java version to 8 * Update to Gradle 6.7.1 Co-authored-by: Jim Anderson --- gradle/wrapper/gradle-wrapper.jar | Bin 58695 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 ++ gradlew.bat | 25 +++++++---------------- lib/build.gradle | 17 ++++++++++----- settings.gradle | 11 ++++++++++ 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2faf2fc91d853cd5d4242b5547257070..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 12842 zcmY+q1ymhDmoSOmQr324hm`&0SZbS3R18r37J8Z0F-T>@gIK` zatajf2b1lO#&E_RnGDF51uk%8Wcxm3`%Yhe7Psx?*?eZ?8M9_+G(?L=s^OG1n#S(NP3gF?2Mr2^f5E7sM~iVC`Rn;(-^MZ zZu*ZXB;XmgvPls(e#)MMTObsEx9oNz-K?AmQ8pP&P7vqx*=5zxjU+ye_1R<%KSg1? z7H&Yh))(Ke!Pa+aVuWxPKa_~Qo_IH}*;tV8n~O*Xa?t3P^9=L%=wOL1=~{LVv}mU8Q#e6s>v}iV8cDP|EdY)`dp≶7^21 ziF~qst3+S0y_IcTmzBD?t^AL=8|hpx>4aXc#L1YriEI=T#&IZ=SoAEyLg|^3d~uWZ zL(@1$!3on^gfz^e5VdZe5qx_>I%?g|J-FS>NG7S8Uwqt9t6KDa`8Nu!bDng+bM`&i zd>s2#sQ2Dsh6c}3YYi}8DqsK)DG!%;@xqz(<#=W`C`X+!HhtF~r~9OsI`@n36>D}N zz^HjPst0d<*2#=afSFiYwBeNZDk>BahnaW;GkQDA235(RJ%j;vVg80O#gk|q<#+OO z!F(BArIYDQG-{DlHpf+F=!)yw08zWccjd6DKgR+zJ(0X3zS;mzg+Na{$2N+AhF7`& zXj`aBWy{YG#8s$C5=GZH$a@!+F42?=O~WoaIjO;k;0P0nE5|ma;I^@xN`kKvIjTQe z1!_si%O1V@BP`r(WwTpr7HN&p#_-)5!T z%!r5ZL79g`v%i29=J2rPglr;%LCc+ZSZeh71?CfOgZ&EJdacV35*58xwhWGhyMhx{ z5KAVHq&&zae)(vc?T~KB9rtcfzy#SAUvce5`+$`_U7}=j*;@5(PyBoTp#IwDtV?s% zQ%T#rekISAFx`AeHyBx6BP^4OtUo>VhbksSk&W=OkQIO#SJ13R8z6r|HNM}$TK=58 z^$>Cg`+P;E@||v&RXQ8dF?fqSS3;wKND5tF(tf3C`q!LEI9_~9LgscI=n#Q>Vl6%6 z^xQ<;f6C*>yStD8WZ4LPzJjmeuu1L`A4BDvEy6DgDMC)PB+3}KWft<^5DPgko{>P8 zJL=zIrDlQ3l54nAxi=;0*HF+cQ`|0Z;~#mt0NHndDI8Ft6^Gp+Lz!19<=L3-abvfX zelFvqpMs+)n2}tXR2j_UG99=i2A)GzpZxTtF=_i+PyVcT4m=oLbh0j3wb~T*1D(f! zOnvTcyI^VbldY>z*{sBnk&j3`-I6GqvB;Qa*bl<5YKpLMNKjDk-$VW9y)f6Qa?T73 z1=aTsLXIW~Xm4p^spI@Hmcn0=j#SgUrQ(LwQu{s6rO7cNL8G>CZWT(hIbdv%x(Jlp zoI&SgpA?j``5vR&m!53m5=zO&hWkznAAO#F&1pI^L3;~$fir#2Cha{-SD4F2dZ$Yj zNWSt;3dLNmPZ<;D0kQqC&yn>qqCMISL58?}bV(f=u%P^DVHARl?p=uptqCK6qHR%G znz@gHYqEnCEL=>78`c?7$>81*%RQ`@urhDyDti}_ZIXnVa)~U{)lq9bj?aBpb1|OX zQEOY8nV`I7nqbYP%pqaNpQZht6Jst`i`{B$ycuhg>p)3{T|=C)ZRx zwhOaI{+g~G@s-nQB66k4ZKP7Wk4v)bVT$sdEEvJj5EkX)2#Rp1J(m+pLGRGtgR}!C zJ1^uNmx6bMEDWh)dOtRzDkdg7lNs7AO6;LFpmezCp}|2dbseLD5M?D7VP+y`GysD~ zXb)?J3jG=5(Rn1_;i`Dqld zLN8F94c4{|1+YfvKa)vn+;*{ju_%uj`H`ke;KQ2P7DD5nGOQP(R8l=AL0{o9qc%9& z4e))*rFyxhsM%wgJC6S4tJLteds>&34_6tvv7a(#F`kk%031W1Aq<#&3|2ZN-Cqq`-l5Ajt zmAD72)g^6kQ@$3=wef)3tC4m)dsw?AxwR=`#N_`9Hd+t$4SzJ+Za8)malG?}{YbGV zxcLZ87Enlr@O~eE@6qx44m*uKyFE-L%FP1HxR_($c}_VqmXk%xb*nPsReTfvRCy#; zLY#`)G1RGkp=-|NJ^jIMW}3=(vjF6sXq}{QLw%AcwOIS4Xzu*SI~An6k)^=@T}{+b z;{p5VP*8g0P*4>A`R0;9;+Nh5H3o>@M5CSo@o)`_E?{vin&S{F5*+l|B+sN&hr~i^ zxo)Y1WCr~t-M*v{c=O$137j0hxQnsK3wkdHI@jz{r>wsxUt;$AWa$ls__3NTo)gRm zxs5xy_-19*m7b*fKPY(QViL^@bJ0u|)*rDFGM{5vt|In|Dbn>J8Y}9zMGS2hhAyEyX;#g( z3$svdx$kb`47#gDE}`MAb8TA7R|g7nS`|htx!e*OH3KH;-=d|O^tcqIH0d%+3iWA0 zc>|NUCIvSN=od!@4k5Y~-RqKc;Mj?P6lV=^P5w4Y*^K}?DsbbI!s~r})~$Z%6UvLI z5j+vg$jfj?7@8$a{2edF8S?`VQ@8Z4q4sv=4Npp2Rk!3}&cKMVgm2Y^BjZl#jZ?}) zxnI}B=kjh{W(VDN$z6XX19np0>bUe=C6Ih6_wN`?Vce#N4D9Rl3fX67_r(uM_$4H;FS0L^8S4SsUjic=MUP==s$OM)&NEvp)gDY#mLjhipsjL4`P4~y+`Ffxn){Z zM1N=8c5d!;9JDPTH^%wTa}r{{C6e<~q%Z-FCbp1aBH&ZH-)oNV1Fn^++vwDsdP6*} zaVhuu2m6!6^tlgaCy^m$Egs|YdX=V|Me#&Rq<3K`OoZI~O5Bm)H(OTPBblGUmJg0| z-izCVizZ(K;ct6*^BV0U#+S@wOf5Zixt#8bM^uTH0|NxC-~Y)n6Xq#4ROj%DVD)8= zBCj(Tvjoy@S#8(io3h46W>%(0?BH3|f~ zZq)Djpdf3=53UQ^^XbRF&r^wwiCCoP-$on0UIe_qQp8l(D;vgp5?-tOGOH$Gjwd~0 zP-c9qN8_Z?BDcLlJvT^o_QFSEa0&@kuTLnrD_uKDAQZ7!g`IO9R9fTT@7Imu&@^@m z?qLv2Y{YygNjB;sk7J*DU>$t@B2QBhPY|r*5cj8Z9cR1l3OW>>kyz_7VIbUnO1*T+ z9R_H!tG(65#gKrw8j9+gx+_FfNZzggvg8ut<>bLdeet7<#JLJ_x1f%XFklxkhk;Q& zlehT2JngNwJRkN<$!~!2&0Ypouxad+=bQtZ!X$UphLDQG_S5)O&__;c_f*|+lp2ZZ zb76mUyr3?H+16hMIi0xCNVPOzqbJg&7(w8MVCzHGtbS&j1f^eEw%Ax_x;-@du9i|; zY=1W0GG4sSZfnWW9v0COT!>0Kp4UDL2Hj+kovXh(BB?mhhdoSJ4=u}gXno7mDu=dC zoEbrTZk!pao1a3}xpEUS8VADYb;P9bB4H%rWa4Mx=Y$I85KhEnNeh!@&^4nf9H9Xm z!v|Iya%#6W8M4A#kbg|B7~F`1Ag0{=1FS6FcG-QC&M0#{?C~|i(mn~Fqr7Sf?Yse5 zuAfH&M+NU zr;=Ulc0TW}u9-^{O7RHY6OPIhyaTZE=(Nl&!jj2uVS5!ZQY2LBqP6e)7&F3Q_Hab_ zLrV(2QNJR@Q3@ySlY`qAJ9RW~ANP~kCZVc+(E_?xFf#1GdC0(ny@RWUMHZ%x#$)ve zcJ}OJcEnWXXK3lgvCS6;RcVVxAN!_E@w<4vAMGFa<$G1RE+wyj%X;u}&YuEpoFBLY zM0*$}OUKT3lDDD0O&TM_#1oBU&8>dbIXCK0$D*1#`;Df(2uT^Ys9whszl=P|xI zK4W*LaJ@*-8JDeOOjy3z`Q?(C2>>3>fNK3wAi&Px<>6wQKf}hc^znUVz-_hJ(=Wd4 z9IgSrq}Qf)hGfeb7E!z>^fAEWN>&Y|bLXLO1^4350UN=XN>k)gBATK}fRry2DzFf> zeI(W{Xb~dAwxAs=Iz=}3s2+eqikF2ZX30Fu3T?1I`cx!0rN1g2`c0fKmhEaZJ7nf# z7i(J~BWxF}HSK0xuHTjRBVugcLHr;P4EsClv;7N>sBvGKacB8^N>1Pj{+H4BO>brw z0Z=^L{Yk5nDlH1J>{YeU!Y6DDQyZmEqf3LCmI8@+&eAWLcdAks4e)z-%Fp|y7pkRL zh}ia&0kgEwac`26TUTAYnw`*P9-Q7OWaElwe(&9MbY7Am?<9OE zFl?kb79&+DH2+NM@b%G?f-GhaeE#IQrH%0x#4(vYW~7*gBkYlZ_UG$X<**g4C=s1F zetpyc6kF;#-gZ{o zo5%FNck~|JmFqChIa4HyzJ;9s^E2Q(q|ExN5GJ_+N&t=PR~t6#B$4n*`iuX z1ar~vtGu~kDGyTq9B@908w}v9v2?bp-!9ig&naSfi|YE`Z$sk-lBk*YOlN|43gU^lO_#1eF&9897yLT7 zYcHsfu2`qSC6AbNB}O{3wPcAu%$O;&a4co+Tp2_=;n%-!MlMGm-@306*aPxS&vEN4 zj(vxTGWQ6asMvUfb*p&*Fs`N}LjI4Z&xgki`R^F0>I4N9QCZ+RXKPC_CuR2&Y^#{2+ z!ilm<k;?lLILj?ya|l9 zOrF)I76r5|z+J+8 ziFg6yfg3{uQbl69k(zf@XXb1Y%n*+s5lv;=~4G5mx-nnZb5v}SQj%*ymKZCt1qOy+hkMR?8 z3gvp`aXHm=DrW6Ny_oLcLxJA%*=Qtx`2sd34-LI3R;| zOyl7p2TBbRhEzz#+0d|LY0{h(3twB_~^+8*w z(MzSza-w44?3^wDf(gH9>2>qWL&SogA z-~dj4T9o)~d0>{1_V$*al&)P&kJE z(;N?J$~(vn9K#c|HbCAO?f$k2SDJQWKaxirtifx|+UMwRCosQtaG|O>CaCtzh+1k_ zUN-Kl6?5rfCS-I>#mU)Ru z`~1=H;0sspAO7fElv->{+Q6+a2p>b zzWKwufce=pd5)@gn41XkS@m8%-2@asCj|(nG2jk~7Sq~Y$}9DXI}3OP5GhER=U&AP z46t6NH}f!7|Jn|2T{;w|BDLC1_ipdm=dMIyzuCbBc>%lXcp#a~kgzS0JDfa0u40m48`rku!q$3Z`?6tz{7*3^*p;uG^uikJU&oxP-l&}0XQc|~h7{Re)JHg*6b%(nxs+$7)^Z;BFV`}*L;>Ih zMs0u!*7d+jPeqM>>`JVZNg&G2h&w?{eiRg}{_C-q$%M!Li&?YZ(2o10ogN!NtSeNC zjIimtk-Li5J5$w6iCygi?yi5%rFx>QPLksnXoCOsKnj zWm@ShV5616Y;`R6(k1=$nob4SvJJy_(#wO@~}HOesm>Yt?QkErQn-m;~p50(~qBvdQO0Tqg#3yus1TtZVpt zez2NfwQCkO9BbTyZKb53b=pgfR5#)@qqG_jn;*jYd8%il*I7t~jol8g3`&N1tZZcU zP?@z6(Ef>6&(i8g=}|}YxuzVGL*SZ}6Xc?$r~90bt*|D*l?gqfI`mopjp%Mhfm?-x z^|Bt(sH}sqdH{8p-4YV~9?TQS?iq*C9y#_-a)8xJ9W+}0A{X$4W6q7yGx!# zioE>n)y?!4H)ge$jK${3)1^DbijD9)Tbm=idENin5;KJ7luQlufA)Y6ykK04aG;=A zS)icEA@z26kQp%oz|5ODGKAd$O^%$&Ocur*fL?wa7`4$$}c9uTHjTP0W!qq|U!ZTXG! zwem4Au0gAe-)dl%%kFQJdq!$^wL1&-O#7CA>qah-HDa=g!Cx@~#P%n-dWGatXH5pk zk`c+UtVD^6d52Jq=ezrLZC@~B>n!Jq_T)DrWX?LLT7^$JoeVtHsWP}AN(G+3&OWvB zI(4}i1CqC`HK;8cZQKq{oi2*sT2YnYWATa7K-%h5+xklmhKb%s_N9oPh`ac0p9$uY z3BUU*z1bEvEi|WFbJ12$SE@|f#%F2^r_OCT8feGbV{pP=MCN*PnKg5M^P+OlOCwFw z?nLdXdPg~8P&5Fp-3Vdn;7aG(I(LjNO-fY!2K-7a*I!t+riEn1v=>-bx$Ua~1Bj+a zAF(54&s&r(8NkRr+XfIwv~g$fxM7+tZw4*5%$~I-pnxQCI@xM9QOeJ=V*gIAP{Dz+)^?Hv zuwfLo(wQwMnKVpXPWE$dD^$WJxp!TtUGHsyrVj0({$@OqbjXyc$x-@JUf#=Uqqbi) zyT#X;Ww$0@Bjh~ATwOgraYm`5Q*M^y+45K`*XB2v+84RTvXBJ&h}vda&w?8Yc8AL~ z{E(zrVR*+rbokD+ihF5}qJC<_c=F%`_~1K77UV3r_yx=XH_jX`KJTEYkJ(jck3EZM zTN~|>DNocbL;@2$a9)Xe{WCc>MTv@bZh0NylBl(x3+y2upl3t7 zQ7zZDZ{h4a^raO-@xk1 z9TpSAw6VztodyVF+}N^t+`@ObqHijM>hLM9<5Cm$oZ4bByuMxEcs3k#se;Ob<;y`{ zqnfp+BI3w$bQh%Zm2*^j#r*Sx0PlHn=rDdx$O^$HD0=yY+DrI*2afM}3sKTZ@{v$O z-))`LxK!);ST-&LCmeR{K^H0?l-DmJlXHfv41D|tq6k}S-gm20i(Z{LNmI^n{J>+P zu zB%Yv;$Pk$%K`K`Vi`gYjjZS2Hnk3~gC}*QCLT;KZ1J--!f>fo73wVt^rrBk9PiE{s z5sNm%+$*S`pjQy9%J73s{&hyJYw8(v9${NeZ#8yu5JwA=ezl1Vbxl9)8ZkwGl362v z*~bq>^-=E|qukP$Mm0G&fvk051!m_ihTqYxyb#9dk=mbPNumYUbkIw!QlCGnl$smp z?Pd0FTDj-HYXak>3#oIM&8n5=wAs#4ma44Our{&10kl=!+tTyQsn+B5IFnLH52(CU zp=XS10t|z;9qb0~&oORiyw67 z*QlVQ@4qfLhFW%QrxT5z)7qI%;-Evg9T}+v->Wv};S)o;a`O4kH-|JI!P6(hWbVZG zu3klVR@UPg!(Xo~05p37>P17&(^+DK)UKQ`b{dp1+2xJ!9=|Yb*WH#q$;66Mks)~W zMmjG)HTiMckF|*bzs=3=`E#6i4GSb{!y+DjpmL|s`+4-nipJ;9kbFZlcL}WZ69mMM z*lyB1-adRRyA^*!a*OvREWFnBd;vdt72M0F!=GewE(`H9SOrwaiKba_ z2auy6$O9LMoP=?7=j@C+8xcc;GTrEwc&#fT#Z95R&vzyOQ7iT?n&nOXTC^koI=)GE z$(dmUqlI4kw;KE+!=tVz(wumguhX!82n+CZIFvnJSc=pG4Q+Hm)3Q${v6l(6TgRPinsg&fK*bQBHc%w@veNmwotb;NZVfgLKZ^#2% zIxyH5z3g|#kQU+yol=TUc44&Ouo4&~*(9|Mth4^Ziw`sodKfG@2tlkZMm|36g9<~Y zWE%=J!`Lbut!g;PM|kaKi#AKUdzP+35Vb)K>IU&~%mgGWmk;j8`q5!&vlSA*M98m8b9sZWplD(I~<0?+~bfN?)r{9JjUZ z9F80S7*a(lDl2}veqYj>63% z>lmBeOXGCiRh7V>Bp`H`v`l32Y2}3|2bcuDO3I%aV4mFBy!A{27_x7Pf07%n(i@eJ zR;Yiy>Te3UH)Hd}mmifLmhNs+H2nEEL;@_Gklm@~{2BQOgQS}Ml7W|PP7>0{&&k{$ zljJ&nLN>xFqFX!R1F;)1Bo1_UO)^yZ;j+GLMdOpbzcZA$gkpckam_KH&fmhifYSzO zXyrf^pR?N8CX{9MZ#qeAdo&4ccDL zQRt^{SOU{mZC}AFVtA~br7&%K74Xd4msMx`!k_TY(=~%unotUH5qP^Q(FPFP zWsL6l4qCNs?i`H^PmD~J^HdiW~& z8oZnipal~XC8Md&EVg)5YTnRVsLCM=1lC=n2CLdH4&Qq9?C;%;h6R4z=I{c9YshCl z-^V%dCZE?>&Oc!z5>c3XI7?70I}oL^kfu`~niM5Q717u8fQ~aR0+=|7Y4rV5i)WIH z##D{*zk(4@F~?nLSX+~$QI6gQ##Oj$#-+Hdval|TN)JY&*Ul_oT;1$iwmYIG5a343 zG39F~7hCJg=Fp^aJa2p@nK=N$fknb=DdHBp#YAiS$jQ*isIX!^gQ0Jpi&qy3%N9}& zizKTkS`I%fZwj;FxNq=0Gk+h3RDT}1QhQES^nt}{i9Kcf^v#X}JQbQ=Jf@tnF_@i+ z+Lft*f;}Hee=F=}=;EV0nWJ5*rct)4iL^MSZFn5Ag5&lp>6lnw@xB1ajN{L6R_qM4 z$pjP>Yo}e@ylmI>4p?1$-S1_~vxAlxv$$z}5|&ZNfedapJEN6Zj3DdFA92{Go#I;( zv?M4UCX=BGZav&L5)K+^iJQswQ_tmu!G;*f`}@{)Id8-lc@8jhrmROub7UL)MsDF@ ziQGSKsu*=wFjsu(zSIMC2piGz?zU_xSc&lxchH?N>8zu=r2Yv=2h-4(@NUorxw`Wr zzq+GpM{ZGOO(e;re{=X5qoJ7y9i^bowl|6+wc^Cgl*zu66P3W8n21l%(QyrVu}YD( z-7{+$7@bq06J75}CoE;~z_U!3ZK@z}zCAIBN#++i!M>BH{6!0VXz;Xff4PDxf%_bK;(#smOVP*Zp*&Gj(tN!!bR0zr6^3C$<+#<{n>3P@zCM zn5(D6FVLC`tmA!4x6lT&)GOh~CgDG}o z-tLW_Bd=~C#zF8Q4vbe*Ky9tB6@TM9u*k9i61T1s^KB;2o8bA{MSX9TfR#z

XiM^r_M1uFTw)(UPqqUJ zed7DJQvOpplYJoNl`A3a2Zr~v)Y}K|*%d7?8;dC*Af}0=(EXrk7hP8PM4v)ZawEv0 z5j5q_6vilv4*ppZ3Wke7%8b`odJu26#YseA?$sP8?@d?TpACTX`G^?%K=Hly9Y$Tj z5`i!}-WH0lQ3yt-&Pf&Z)OxjKfAZ3X`YI2)5o@9EiOA}?kX`^rk;yaOh^Li4VHcT& zd6ztJz^`H!8>cL)a%?Lnofzo0$WPrgkNcP#u@{%~EvA7g#go z+Q4$Q^o_MJTE0G_&9@cgp)WF>2zo>AG}C|(~E^_?ijc2EQ>> zFH1Lxc@i!gDHxvEsrfc+Kiz3Q6Z*NM+U6DH6*-Fv{YDY4>U&eegGH~Xmk~!sd6fw2 z!6^4(MZWhzf(xs6BHy=oS+dir0_JW(j*AIu$9&&p@}T;&6s7(yYidEdkp6pk9}Z(F zk6lG`d!HcJWP{7X)&P5FW;XWU6;zke2e+g*#1kW4L0Auj5pVA45Bhzt{8lj)XyMHu z0p;Q}t*NLnXbGQOTC9WdOQsX;_yLThOP$mzcEbqSBifmDecV;Fqhtm#KxfJTMhY!K zw{1L9za2QtuIWXu0ZYm@Uy72(9^vYajuP$(VAKn+&Jp z!%S;#BqO;02)n~6pFYK8oRC6wXx@kyb?VENM7l?NFg)%^=q4aZ* zwVCZxA8lalF&n`vk#gxD@!9~Aktc+h2UUUax3q2XKQLuL@CpVEpx2IyiH3bALY+Oi zDydtacE1Z|$z5qsCG=%F{}8_ozwieWuM4VcDesuu+wX&YjHm^Ryx)pVtiSLpch7<` zIw9QM%I;(VG9~#Rw2JNt+;?_=O2lA#YSudpgsM`e&(}1 zGdj7U)St4`+Cj#Vn>a;Bj~Q3S1G54;gzq)`b_Hh~ip(`BK#GMkI*RUcK+6ycjl?QN zP%sH5*R$$6A&8j8O4iFJhuHMXLJm|drH+=!_{`60&+tC8*m3Z=YsR@~%eaZyWQI|W zie7;BRL6`LR+4B{%FY~;-^*1GGTCLp!U6VoS36GUAWv$Rcc#|a7DD01{QPB=VyT)? z+1ZV>P=bP1;>v-+j0;BwenO`EKcIbmB3$r*@a~=?5#KB_R$3bTg zOjEutZmWuylIJQEM?28D!4uA#0CZEJHar0TFAW|NwP;WL|EIb_L2>;}e*N!K9E61S zH&u@m!n#CH{C_j}{#ybCRU8z7`Cs{b>@bxSkp3lOm`qAYgB>npnw%V>w}t_+S_Z)s zFhGKq^e%m@=^@sD0CJdAK=`Upbr}?}N zf;snqpt#dOwhxFg82(%Tw=ND+@`O0JGeOWd7-3R8A%Yu%FhiaYXD>p?t2+o%_1CKE z{g(>=g%}X(O%M!}KZK%$7-F<34wD-24`y$WLDv6z?ty=_b)Oi*x&?v}3j0f`AV3HL zWPAYw!XNs-1EmZ9=d=$6LAJISVJ4&gQM5=bh{!f0OmFNz8oMnGgOl_RK5VPP3@87C zpLS$muCo5YTmOslSjFLb`AXIJ@1|O{pm5r z9MxUb-8HMe*|TpFa%dE?+7}MVQ-A-N8wvtq85ROU2%KYt4etC52X0%W08haQgGSU| zvw=K$djTYSLy@4My_Te_8JcZp8Oozg{-ey>*Nm7BkGrX(M~HU6N16U>DSj<`;cy`u zR?0B23zx|*o1V=#fLZ=wd6*NfWjC`pqA^mt>DTZjGBeX`0ZK>Qgxz+37Dyb#NT4WT zl@7Lmh{WdY*h%e_bo6(oXKw=`(9=mft10fOR4< zCOUungtJZPAM~qw4Ydy9PhvYTi1ZRqR5nY)?OX2O3FJ6VXy&`&H%^U|d>N>@f zQ}Cd;DV}_bNiVTW8HcUJDRvXCUwGx|(r2-KbXY>jpd!~jmt(

p*eWM!w;MI3hb2}DlKZV3$r+FsPh`1u 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 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/lib/build.gradle b/lib/build.gradle index 9ea22404..eebfe917 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,7 +1,8 @@ plugins { - id "com.jfrog.bintray" version "1.8.4" - id "com.auth0.gradle.oss-library.java" version "0.11.0" - id "jacoco" + id 'java' + id 'jacoco' + id 'com.jfrog.bintray' + id 'com.auth0.gradle.oss-library.java' } logger.lifecycle("Using version ${version} for ${group}.${name}") @@ -28,9 +29,15 @@ oss { } } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} + compileJava { - sourceCompatibility '1.7' - targetCompatibility '1.7' + sourceCompatibility '1.8' + targetCompatibility '1.8' } dependencies { diff --git a/settings.gradle b/settings.gradle index ce7f0a64..d3c7fb14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + jcenter() + } + plugins { + id 'com.jfrog.bintray' version '1.8.5' + id 'com.auth0.gradle.oss-library.java' version '0.12.1' + } +} + include ':java-jwt' project(':java-jwt').projectDir = new File(rootProject.projectDir, '/lib') \ No newline at end of file From 58439f41bed7ead6911e47cf71dfb3de59d5cb12 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 18 Dec 2020 08:27:03 -0600 Subject: [PATCH 109/293] Release 3.12.0 --- CHANGELOG.md | 12 ++++++++++++ README.md | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39da33e9..12ab335a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [3.12.0](https://github.com/auth0/java-jwt/tree/3.12.0) (2020-12-18) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.11.0...3.12.0) + +**Changed** +- Thread-safe classes should be Shared statically [\#462](https://github.com/auth0/java-jwt/pull/462) ([LeeHainie](https://github.com/LeeHainie)) + +**Security** +- Update jackson-databind to 2.10.5.1 (fixes CVE-2020-25649) [\#463](https://github.com/auth0/java-jwt/pull/463) ([overheadhunter](https://github.com/overheadhunter)) + +**Breaking changes** +- Target Java 8 [\#455](https://github.com/auth0/java-jwt/pull/455) ([lbalmaceda](https://github.com/lbalmaceda)) + ## [3.11.0](https://github.com/auth0/java-jwt/tree/3.11.0) (2020-09-25) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.3...3.11.0) diff --git a/README.md b/README.md index 712cb07d..618fa12e 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.11.0 + 3.12.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.11.0' +implementation 'com.auth0:java-jwt:3.12.0' ``` ## Available Algorithms From 63812af383245d526c0f89ec02abf861967af031 Mon Sep 17 00:00:00 2001 From: darveshsingh <56809416+darveshsingh@users.noreply.github.com> Date: Wed, 16 Dec 2020 19:09:15 -0500 Subject: [PATCH 110/293] Fix: updated jackson-databind to 2.10.0.pr3 to block CVE-2020-25649 ## Changes Version bump of jackson-databind to 2.11.0 ## References: [CVE-2020-25649](https://bugzilla.redhat.com/show_bug.cgi?id=1887664) --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index eebfe917..fd920e2c 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -41,7 +41,7 @@ compileJava { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.5.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0' implementation 'commons-codec:commons-codec:1.14' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' From 3b87927a8e449d867a1312086164afd08c56bda7 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 20 Jan 2021 13:00:52 -0600 Subject: [PATCH 111/293] Release 3.12.1 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12ab335a..fd2e477e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.12.1](https://github.com/auth0/java-jwt/tree/3.12.1) (2021-01-20) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.12.0...3.12.1) + +**Changed** +- Update jackson-databind to 2.11.0 [\#464](https://github.com/auth0/java-jwt/pull/464) ([darveshsingh](https://github.com/darveshsingh)) + ## [3.12.0](https://github.com/auth0/java-jwt/tree/3.12.0) (2020-12-18) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.11.0...3.12.0) diff --git a/README.md b/README.md index 618fa12e..90c71bbf 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.12.0 + 3.12.1 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.12.0' +implementation 'com.auth0:java-jwt:3.12.1' ``` ## Available Algorithms From 9e951cd9fd63a98b0a9eb5a0367f4cf840952306 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Thu, 28 Jan 2021 15:33:46 -0600 Subject: [PATCH 112/293] Add toString to Claim objects --- lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java | 5 +++++ lib/src/main/java/com/auth0/jwt/impl/NullClaim.java | 5 +++++ .../test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java | 7 +++++++ lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java | 4 ++++ 4 files changed, 21 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 4e2aef63..ae266798 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -128,6 +128,11 @@ public boolean isNull() { return false; } + @Override + public String toString() { + return data.toString(); + } + /** * Helper method to extract a Claim from the given JsonNode tree. * diff --git a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java index 93bedbb1..8d10ca10 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java @@ -65,4 +65,9 @@ public Map asMap() throws JWTDecodeException { public T as(Class tClazz) throws JWTDecodeException { return null; } + + @Override + public String toString() { + return "Null Claim"; + } } diff --git a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java index 1a82fcec..84d0be4b 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -431,4 +431,11 @@ public void shouldReturnNonNullClaimWhenParsingBooleanValue() throws Exception { assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); } + + @Test + public void shouldDelegateToJsonNodeToString() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + Claim claim = claimFromNode(value); + assertThat(claim.toString(), is(value.toString())); + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java index d8ddd516..b15f3dad 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java @@ -70,4 +70,8 @@ public void shouldGetAsCustomClass() throws Exception { assertThat(claim.as(Object.class), is(nullValue())); } + @Test + public void shouldHaveToString() { + assertThat(claim.toString(), is("Null Claim")); + } } \ No newline at end of file From 7fe888ca1d3f9d5918f6247a17763d70c52b5607 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 1 Feb 2021 11:43:46 -0600 Subject: [PATCH 113/293] Update acceptedIssuedAt JavaDocs --- .../java/com/auth0/jwt/interfaces/Verification.java | 5 +++++ lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index e23f4340..f9904aac 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -64,6 +64,11 @@ public interface Verification { /** * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. + * This method overrides the value set with {@link #acceptLeeway(long)}. + * If Issued At verification has been disabled with {@link #ignoreIssuedAt()}, no verification of the Issued At + * claim will be performed, and this method has no effect. + * + * Issued At Date is verified by default when the value is present, unless disabled * Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway * * @param leeway the window in seconds in which the Issued At Claim will still be valid. diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 156a39a6..b8fd2f5e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -672,12 +672,14 @@ public void shouldThrowOnInvalidIssuedAtIfPresent() throws Exception { @Test public void shouldOverrideAcceptIssuedAtWhenIgnoreIssuedAtFlagPassedAndSkipTheVerification() throws Exception { Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 10000)); String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - DecodedJWT jwt = verification.acceptIssuedAt(20).ignoreIssuedAt() - .build() + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) + .acceptIssuedAt(1) + .ignoreIssuedAt(); + DecodedJWT jwt = verification + .build(clock) .verify(token); assertThat(jwt, is(notNullValue())); From 3766cd8c4d0ed87b91f29ed314744f0db8e58f37 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Tue, 2 Feb 2021 12:23:20 -0600 Subject: [PATCH 114/293] Update JavaDoc to remove duplicated information, improve clarity --- .../main/java/com/auth0/jwt/interfaces/Verification.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index f9904aac..6821f3ae 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -65,11 +65,8 @@ public interface Verification { /** * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. * This method overrides the value set with {@link #acceptLeeway(long)}. - * If Issued At verification has been disabled with {@link #ignoreIssuedAt()}, no verification of the Issued At - * claim will be performed, and this method has no effect. - * - * Issued At Date is verified by default when the value is present, unless disabled - * Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * By default, the Issued At claim is always verified when the value is present, unless disabled with {@link #ignoreIssuedAt()}. + * If Issued At verification has been disabled, no verification of the Issued At claim will be performed, and this method has no effect. * * @param leeway the window in seconds in which the Issued At Claim will still be valid. * @return this same Verification instance. From 2ee2b3c8e131210b417e66ce7acdd88fdc06c0d6 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 1 Feb 2021 14:48:04 -0600 Subject: [PATCH 115/293] Add ability to verify an audience contains at least one --- .../main/java/com/auth0/jwt/JWTVerifier.java | 57 ++++++- .../auth0/jwt/interfaces/Verification.java | 24 ++- .../java/com/auth0/jwt/JWTVerifierTest.java | 158 +++++++++++++++++- 3 files changed, 232 insertions(+), 7 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index a311ab33..2393f76c 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -11,6 +11,7 @@ import com.auth0.jwt.interfaces.Verification; import java.util.*; +import java.util.stream.Collectors; /** * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches. @@ -23,12 +24,14 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { final Map claims; private final Clock clock; private final JWTParser parser; + private final AudienceVerificationStrategy audienceVerificationStrategy; - JWTVerifier(Algorithm algorithm, Map claims, Clock clock) { + JWTVerifier(Algorithm algorithm, Map claims, Clock clock, AudienceVerificationStrategy audienceVerificationStrategy) { this.algorithm = algorithm; this.claims = Collections.unmodifiableMap(claims); this.clock = clock; this.parser = new JWTParser(); + this.audienceVerificationStrategy = audienceVerificationStrategy; } /** @@ -47,6 +50,7 @@ public static class BaseVerification implements Verification { private final Map claims; private long defaultLeeway; private boolean ignoreIssuedAt; + private AudienceVerificationStrategy audienceVerificationStrategy = AudienceVerificationStrategy.UNSET; BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { @@ -72,6 +76,20 @@ public Verification withSubject(String subject) { @Override public Verification withAudience(String... audience) { + if (audienceVerificationStrategy == AudienceVerificationStrategy.CONTAINS) { + throw new IllegalStateException("Audience validation behavior has already been configured."); + } + audienceVerificationStrategy = AudienceVerificationStrategy.EXACT; + requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + return this; + } + + @Override + public Verification withAnyOfAudience(String... audience) { + if (audienceVerificationStrategy == AudienceVerificationStrategy.EXACT) { + throw new IllegalStateException("Audience validation behavior has already been configured."); + } + audienceVerificationStrategy = AudienceVerificationStrategy.CONTAINS; requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); return this; } @@ -200,7 +218,7 @@ public JWTVerifier build() { */ public JWTVerifier build(Clock clock) { addLeewayToDateClaims(); - return new JWTVerifier(algorithm, claims, clock); + return new JWTVerifier(algorithm, claims, clock, audienceVerificationStrategy); } private void assertPositive(long leeway) { @@ -412,8 +430,19 @@ private void assertDateIsPast(Date date, long leeway, Date today) { } private void assertValidAudienceClaim(List audience, List value) { - if (audience == null || !audience.containsAll(value)) { - throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); + String invalidMessage = "The Claim 'aud' value doesn't contain the required audience."; + if (audience == null) { + throw new InvalidClaimException(invalidMessage); + } + + if (audienceVerificationStrategy == AudienceVerificationStrategy.CONTAINS) { + if (Collections.disjoint(audience, value)) { + throw new InvalidClaimException(invalidMessage); + } + } else { + if (!audience.containsAll(value)) { + throw new InvalidClaimException(invalidMessage); + } } } @@ -438,4 +467,24 @@ public static NonEmptyClaim getInstance() { return nonEmptyClaim; } } + + /** + * Represents how the audience will be validated. + */ + enum AudienceVerificationStrategy { + /** + * No audience validation configured. + */ + UNSET, + + /** + * The JWT audience must match the expected audiences exactly. + */ + EXACT, + + /** + * The JWT audience must contain at least one of the expected audiences. + */ + CONTAINS + } } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 6821f3ae..2130fcd1 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -25,13 +25,35 @@ public interface Verification { Verification withSubject(String subject); /** - * Require a specific Audience ("aud") claim. + * Require a specific Audience ("aud") claim. If multiple audiences are specified, they must all be present + * in the "aud" claim. + * An {@linkplain IllegalStateException} will be thrown if this Verification instance has already been + * configured with {@link #withAnyOfAudience(String...)} * * @param audience the required Audience value * @return this same Verification instance. */ Verification withAudience(String... audience); + /** + * Require that the Audience ("aud") claim contain at least one of the specified audiences. + * An {@linkplain IllegalStateException} will be thrown if this Verification instance has already been + * configured with {@link #withAudience(String...)} + * + * @apiNote This method was added after the interface was released. + * It is defined as a default method for compatibility reasons. + * From version 4.0 on, the method will be abstract and all implementations of this interface + * will have to provide their own implementation. + * + * @implSpec The default implementation throws an {@linkplain UnsupportedOperationException}. + * + * @param audience the required Audience value for which the "aud" claim must contain at least one value. + * @return this same Verification instance. + */ + default Verification withAnyOfAudience(String... audience) { + throw new UnsupportedOperationException("withAnyOfAudience"); + } + /** * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. * Setting a specific leeway value on a given Claim will override this value for that Claim. diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index b8fd2f5e..1847824e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -111,6 +111,7 @@ public void shouldThrowOnInvalidSubject() throws Exception { @Test public void shouldValidateAudience() throws Exception { + // Token 'aud': ["Mark"] String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience("Mark") @@ -128,6 +129,32 @@ public void shouldValidateAudience() throws Exception { assertThat(jwtArr, is(notNullValue())); } + @Test + public void shouldThrowIfConfiguringAllOfAfterAnyOfAudienceValidation() { + exception.expect(IllegalStateException.class); + exception.expectMessage("Audience validation behavior has already been configured."); + + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Mark") + .withAudience("Mark") + .build() + .verify(token); + } + + @Test + public void shouldThrowIfConfiguringAnyOfAfterAllOfAudienceValidation() { + exception.expect(IllegalStateException.class); + exception.expectMessage("Audience validation behavior has already been configured."); + + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience("Mark") + .withAnyOfAudience("Mark") + .build() + .verify(token); + } + @Test public void shouldAcceptPartialAudience() throws Exception { //Token 'aud' = ["Mark", "David", "John"] @@ -141,9 +168,70 @@ public void shouldAcceptPartialAudience() throws Exception { } @Test - public void shouldThrowOnInvalidAudience() throws Exception { + public void shouldAcceptOneOfAudience() { + // Token 'aud': ["Mark"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Mark") + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + + // Token 'aud' = ["Mark", "David", "John"] + String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("John", "Jim") + .build() + .verify(tokenArr); + + assertThat(jwtArr, is(notNullValue())); + } + + @Test + public void shouldAcceptAudienceWhenAnAudienceAndAllContained() { + // Token 'aud' = ["Mark", "David", "John"] + String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Mark", "David", "John") + .build() + .verify(tokenArr); + + assertThat(jwtArr, is(notNullValue())); + } + + @Test + public void shouldThrowWhenAudienceHasNoneOfAcceptOneOfAudience() { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); + + // Token 'aud' = ["Mark", "David", "John"] + String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Joe", "Jim") + .build() + .verify(tokenArr); + } + + @Test + public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() throws Exception { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); + + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience("Mark", "Joe") + .build() + .verify(token); + } + + @Test + public void shouldThrowWhenAudienceClaimIsNull() throws Exception { exception.expect(InvalidClaimException.class); exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); + + // Token 'aud': null String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience("nope") @@ -151,6 +239,19 @@ public void shouldThrowOnInvalidAudience() throws Exception { .verify(token); } + @Test + public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() throws Exception { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); + + // Token 'aud': null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("nope") + .build() + .verify(token); + } + @Test public void shouldRemoveAudienceWhenPassingNullReference() throws Exception { Algorithm algorithm = mock(Algorithm.class); @@ -184,6 +285,39 @@ public void shouldRemoveAudienceWhenPassingNullReference() throws Exception { assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); } + @Test + public void shouldRemoveAudienceWhenPassingNullReferenceWithAnyOfAudience() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience((String) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience((String[]) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience() + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + String emptyAud = " "; + verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience(emptyAud) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); + } + @Test public void shouldRemoveAudienceWhenPassingNull() throws Exception { Algorithm algorithm = mock(Algorithm.class); @@ -204,6 +338,26 @@ public void shouldRemoveAudienceWhenPassingNull() throws Exception { assertThat(verifier.claims, not(hasKey("aud"))); } + @Test + public void shouldRemoveAudienceWhenPassingNullWithAnyAudience() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience("John") + .withAnyOfAudience((String) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + + verifier = JWTVerifier.init(algorithm) + .withAnyOfAudience("John") + .withAnyOfAudience((String[]) null) + .build(); + + assertThat(verifier.claims, is(notNullValue())); + assertThat(verifier.claims, not(hasKey("aud"))); + } + @Test public void shouldThrowOnNullCustomClaimName() throws Exception { exception.expect(IllegalArgumentException.class); @@ -297,7 +451,7 @@ public void shouldThrowOnInvalidCustomClaimValue() throws Exception { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; Map map = new HashMap<>(); map.put("name", new Object()); - JWTVerifier verifier = new JWTVerifier(Algorithm.HMAC256("secret"), map, new ClockImpl()); + JWTVerifier verifier = new JWTVerifier(Algorithm.HMAC256("secret"), map, new ClockImpl(), JWTVerifier.AudienceVerificationStrategy.EXACT); verifier.verify(token); } From cbb00491d4b05a5f57d0d5f9c83209a9a9f09f64 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 3 Feb 2021 16:35:58 -0600 Subject: [PATCH 116/293] Manage audience validation strategy with internal map, allow strategies to be overridden --- .../main/java/com/auth0/jwt/JWTVerifier.java | 89 ++++++----------- .../auth0/jwt/interfaces/Verification.java | 10 +- .../java/com/auth0/jwt/JWTVerifierTest.java | 99 +++++++++---------- 3 files changed, 84 insertions(+), 114 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 2393f76c..16a7c89f 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -24,14 +24,15 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { final Map claims; private final Clock clock; private final JWTParser parser; - private final AudienceVerificationStrategy audienceVerificationStrategy; - JWTVerifier(Algorithm algorithm, Map claims, Clock clock, AudienceVerificationStrategy audienceVerificationStrategy) { + static final String AUDIENCE_EXACT = "AUDIENCE_EXACT"; + static final String AUDIENCE_CONTAINS = "AUDIENCE_CONTAINS"; + + JWTVerifier(Algorithm algorithm, Map claims, Clock clock) { this.algorithm = algorithm; this.claims = Collections.unmodifiableMap(claims); this.clock = clock; this.parser = new JWTParser(); - this.audienceVerificationStrategy = audienceVerificationStrategy; } /** @@ -50,7 +51,6 @@ public static class BaseVerification implements Verification { private final Map claims; private long defaultLeeway; private boolean ignoreIssuedAt; - private AudienceVerificationStrategy audienceVerificationStrategy = AudienceVerificationStrategy.UNSET; BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { @@ -76,21 +76,15 @@ public Verification withSubject(String subject) { @Override public Verification withAudience(String... audience) { - if (audienceVerificationStrategy == AudienceVerificationStrategy.CONTAINS) { - throw new IllegalStateException("Audience validation behavior has already been configured."); - } - audienceVerificationStrategy = AudienceVerificationStrategy.EXACT; - requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + claims.remove(AUDIENCE_CONTAINS); + requireClaim(AUDIENCE_EXACT, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); return this; } @Override public Verification withAnyOfAudience(String... audience) { - if (audienceVerificationStrategy == AudienceVerificationStrategy.EXACT) { - throw new IllegalStateException("Audience validation behavior has already been configured."); - } - audienceVerificationStrategy = AudienceVerificationStrategy.CONTAINS; - requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + claims.remove(AUDIENCE_EXACT); + requireClaim(AUDIENCE_CONTAINS, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); return this; } @@ -218,7 +212,7 @@ public JWTVerifier build() { */ public JWTVerifier build(Clock clock) { addLeewayToDateClaims(); - return new JWTVerifier(algorithm, claims, clock, audienceVerificationStrategy); + return new JWTVerifier(algorithm, claims, clock); } private void assertPositive(long leeway) { @@ -323,31 +317,36 @@ private void verifyClaims(DecodedJWT jwt, Map claims) throws Tok } } - private void verifyClaimValues(DecodedJWT jwt, Map.Entry entry) { - switch (entry.getKey()) { - case PublicClaims.AUDIENCE: - assertValidAudienceClaim(jwt.getAudience(), (List) entry.getValue()); + private void verifyClaimValues(DecodedJWT jwt, Map.Entry expectedClaim) { + switch (expectedClaim.getKey()) { + // We use custom keys for audience in the expected claims to differentiate between validating that the audience + // contains all expected values, or validating that the audience contains at least one of the expected values. + case AUDIENCE_EXACT: + assertValidAudienceClaim(jwt.getAudience(), (List) expectedClaim.getValue(), true); + break; + case AUDIENCE_CONTAINS: + assertValidAudienceClaim(jwt.getAudience(), (List) expectedClaim.getValue(), false); break; case PublicClaims.EXPIRES_AT: - assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true); + assertValidDateClaim(jwt.getExpiresAt(), (Long) expectedClaim.getValue(), true); break; case PublicClaims.ISSUED_AT: - assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false); + assertValidDateClaim(jwt.getIssuedAt(), (Long) expectedClaim.getValue(), false); break; case PublicClaims.NOT_BEFORE: - assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false); + assertValidDateClaim(jwt.getNotBefore(), (Long) expectedClaim.getValue(), false); break; case PublicClaims.ISSUER: - assertValidIssuerClaim(jwt.getIssuer(), (List) entry.getValue()); + assertValidIssuerClaim(jwt.getIssuer(), (List) expectedClaim.getValue()); break; case PublicClaims.JWT_ID: - assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue()); + assertValidStringClaim(expectedClaim.getKey(), jwt.getId(), (String) expectedClaim.getValue()); break; case PublicClaims.SUBJECT: - assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue()); + assertValidStringClaim(expectedClaim.getKey(), jwt.getSubject(), (String) expectedClaim.getValue()); break; default: - assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue()); + assertValidClaim(jwt.getClaim(expectedClaim.getKey()), expectedClaim.getKey(), expectedClaim.getValue()); break; } } @@ -429,20 +428,10 @@ private void assertDateIsPast(Date date, long leeway, Date today) { } } - private void assertValidAudienceClaim(List audience, List value) { - String invalidMessage = "The Claim 'aud' value doesn't contain the required audience."; - if (audience == null) { - throw new InvalidClaimException(invalidMessage); - } - - if (audienceVerificationStrategy == AudienceVerificationStrategy.CONTAINS) { - if (Collections.disjoint(audience, value)) { - throw new InvalidClaimException(invalidMessage); - } - } else { - if (!audience.containsAll(value)) { - throw new InvalidClaimException(invalidMessage); - } + private void assertValidAudienceClaim(List audience, List values, boolean shouldContainAll) { + if (audience == null || (shouldContainAll && !audience.containsAll(values)) || + (!shouldContainAll && Collections.disjoint(audience, values))) { + throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); } } @@ -467,24 +456,4 @@ public static NonEmptyClaim getInstance() { return nonEmptyClaim; } } - - /** - * Represents how the audience will be validated. - */ - enum AudienceVerificationStrategy { - /** - * No audience validation configured. - */ - UNSET, - - /** - * The JWT audience must match the expected audiences exactly. - */ - EXACT, - - /** - * The JWT audience must contain at least one of the expected audiences. - */ - CONTAINS - } } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 2130fcd1..e778d81b 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -27,8 +27,9 @@ public interface Verification { /** * Require a specific Audience ("aud") claim. If multiple audiences are specified, they must all be present * in the "aud" claim. - * An {@linkplain IllegalStateException} will be thrown if this Verification instance has already been - * configured with {@link #withAnyOfAudience(String...)} + * + * If this is used in conjunction with {@link #withAnyOfAudience(String...)}, whichever one is configured last will + * determine the audience validation behavior. * * @param audience the required Audience value * @return this same Verification instance. @@ -37,8 +38,9 @@ public interface Verification { /** * Require that the Audience ("aud") claim contain at least one of the specified audiences. - * An {@linkplain IllegalStateException} will be thrown if this Verification instance has already been - * configured with {@link #withAudience(String...)} + * + * If this is used in conjunction with {@link #withAudience(String...)}, whichever one is configured last will + * determine the audience validation behavior. * * @apiNote This method was added after the interface was released. * It is defined as a default method for compatibility reasons. diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 1847824e..131b389e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -6,6 +6,7 @@ import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.Clock; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -110,7 +111,7 @@ public void shouldThrowOnInvalidSubject() throws Exception { } @Test - public void shouldValidateAudience() throws Exception { + public void shouldAcceptAudienceWhenWithAudienceContainsAll() throws Exception { // Token 'aud': ["Mark"] String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) @@ -120,6 +121,7 @@ public void shouldValidateAudience() throws Exception { assertThat(jwt, is(notNullValue())); + // Token 'aud': ["Mark", "David"] String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIl19.6WfbIt8m61f9WlCYIQn5CThvw4UNyC66qrPaoinfssw"; DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience("Mark", "David") @@ -130,58 +132,55 @@ public void shouldValidateAudience() throws Exception { } @Test - public void shouldThrowIfConfiguringAllOfAfterAnyOfAudienceValidation() { - exception.expect(IllegalStateException.class); - exception.expectMessage("Audience validation behavior has already been configured."); + public void shouldAllowWithAnyOfAudienceVerificationToOverrideWithAudience() { + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAudience("Mark", "Jim"); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAnyOfAudience("Mark") - .withAudience("Mark") - .build() - .verify(token); - } + Exception exception = null; + try { + verification.build().verify(token); + } catch (Exception e) { + exception = e; - @Test - public void shouldThrowIfConfiguringAnyOfAfterAllOfAudienceValidation() { - exception.expect(IllegalStateException.class); - exception.expectMessage("Audience validation behavior has already been configured."); + } - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("Mark") - .withAnyOfAudience("Mark") - .build() - .verify(token); + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(InvalidClaimException.class))); + assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + + DecodedJWT jwt = verification.withAnyOfAudience("Mark", "Jim").build().verify(token); + assertThat(jwt, is(notNullValue())); } @Test - public void shouldAcceptPartialAudience() throws Exception { - //Token 'aud' = ["Mark", "David", "John"] - String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; - DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("John") - .build() - .verify(tokenArr); + public void shouldAllowWithAudienceVerificationToOverrideWithAnyOfAudience() { + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAnyOfAudience("Jim"); - assertThat(jwtArr, is(notNullValue())); - } + Exception exception = null; + try { + verification.build().verify(token); + } catch (Exception e) { + exception = e; - @Test - public void shouldAcceptOneOfAudience() { - // Token 'aud': ["Mark"] - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAnyOfAudience("Mark") - .build() - .verify(token); + } + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(InvalidClaimException.class))); + assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + + DecodedJWT jwt = verification.withAudience("Mark").build().verify(token); assertThat(jwt, is(notNullValue())); + } + @Test + public void shouldAcceptAudienceWhenWithAudienceAndPartialExpected() throws Exception { // Token 'aud' = ["Mark", "David", "John"] String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAnyOfAudience("John", "Jim") + .withAudience("John") .build() .verify(tokenArr); @@ -189,7 +188,7 @@ public void shouldAcceptOneOfAudience() { } @Test - public void shouldAcceptAudienceWhenAnAudienceAndAllContained() { + public void shouldAcceptAudienceWhenAnyOfAudienceAndAllContained() { // Token 'aud' = ["Mark", "David", "John"] String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) @@ -201,7 +200,7 @@ public void shouldAcceptAudienceWhenAnAudienceAndAllContained() { } @Test - public void shouldThrowWhenAudienceHasNoneOfAcceptOneOfAudience() { + public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() { exception.expect(InvalidClaimException.class); exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); @@ -260,21 +259,21 @@ public void shouldRemoveAudienceWhenPassingNullReference() throws Exception { .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); verifier = JWTVerifier.init(algorithm) .withAudience((String[]) null) .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); verifier = JWTVerifier.init(algorithm) .withAudience() .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); String emptyAud = " "; verifier = JWTVerifier.init(algorithm) @@ -282,7 +281,7 @@ public void shouldRemoveAudienceWhenPassingNullReference() throws Exception { .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); + assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_EXACT, Collections.singletonList(emptyAud))); } @Test @@ -293,21 +292,21 @@ public void shouldRemoveAudienceWhenPassingNullReferenceWithAnyOfAudience() thro .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); verifier = JWTVerifier.init(algorithm) .withAnyOfAudience((String[]) null) .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); verifier = JWTVerifier.init(algorithm) .withAnyOfAudience() .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); String emptyAud = " "; verifier = JWTVerifier.init(algorithm) @@ -315,7 +314,7 @@ public void shouldRemoveAudienceWhenPassingNullReferenceWithAnyOfAudience() thro .build(); assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); + assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_CONTAINS, Collections.singletonList(emptyAud))); } @Test @@ -451,7 +450,7 @@ public void shouldThrowOnInvalidCustomClaimValue() throws Exception { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; Map map = new HashMap<>(); map.put("name", new Object()); - JWTVerifier verifier = new JWTVerifier(Algorithm.HMAC256("secret"), map, new ClockImpl(), JWTVerifier.AudienceVerificationStrategy.EXACT); + JWTVerifier verifier = new JWTVerifier(Algorithm.HMAC256("secret"), map, new ClockImpl()); verifier.verify(token); } From 9ef20ad39eb9bdb6dc53aff88325553c6630accd Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 5 Feb 2021 09:50:18 -0600 Subject: [PATCH 117/293] Remove apiNote and implSpec JavaDoc tags - not available in java8 --- .../java/com/auth0/jwt/interfaces/Verification.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index e778d81b..654b466e 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -42,12 +42,12 @@ public interface Verification { * If this is used in conjunction with {@link #withAudience(String...)}, whichever one is configured last will * determine the audience validation behavior. * - * @apiNote This method was added after the interface was released. - * It is defined as a default method for compatibility reasons. - * From version 4.0 on, the method will be abstract and all implementations of this interface - * will have to provide their own implementation. + * Note: This method was added after the interface was released. + * It is defined as a default method for compatibility reasons. + * From version 4.0 on, the method will be abstract and all implementations of this interface + * will have to provide their own implementation. * - * @implSpec The default implementation throws an {@linkplain UnsupportedOperationException}. + * The default implementation throws an {@linkplain UnsupportedOperationException}. * * @param audience the required Audience value for which the "aud" claim must contain at least one value. * @return this same Verification instance. From 87a66bf7c94afd4b746d05b303cfd120dca96e4a Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 5 Feb 2021 12:55:29 -0600 Subject: [PATCH 118/293] Release 3.13.0 --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2e477e..887528ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [3.13.0](https://github.com/auth0/java-jwt/tree/3.13.0) (2021-02-05) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.12.1...3.13.0) + +**Added** +- Add ability to verify audience contains at least one of those expected [\#472](https://github.com/auth0/java-jwt/pull/472) ([jimmyjames](https://github.com/jimmyjames)) +- Add toString to Claim objects [SDK-2225] [\#469](https://github.com/auth0/java-jwt/pull/469) ([jimmyjames](https://github.com/jimmyjames)) + ## [3.12.1](https://github.com/auth0/java-jwt/tree/3.12.1) (2021-01-20) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.12.0...3.12.1) diff --git a/README.md b/README.md index 90c71bbf..6f1e0e81 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.12.1 + 3.13.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.12.1' +implementation 'com.auth0:java-jwt:3.13.0' ``` ## Available Algorithms From 94f5fa4291948c0b3274959558854ecbbbb3393f Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 19 Feb 2021 14:10:53 -0600 Subject: [PATCH 119/293] Add withPayload to JWTCreator.Builder --- README.md | 10 ++ .../main/java/com/auth0/jwt/JWTCreator.java | 57 +++++- .../java/com/auth0/jwt/JWTCreatorTest.java | 170 +++++++++++++++++- 3 files changed, 230 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6f1e0e81..295dbd73 100644 --- a/README.md +++ b/README.md @@ -373,6 +373,16 @@ String token = JWT.create() .sign(algorithm); ``` +You can also create a JWT by calling `withPayload()` and passing a map of claim names to values: + +```java +Map payloadClaims = new HashMap<>(); +payloadClaims.put("@context", "https://auth0.com/"); +String token = JWT.create() + .withPayload(payloadClaims) + .sign(algorithm); +``` + You can also verify custom Claims on the `JWT.require()` by calling `withClaim()` and passing both the name and the required value. ```java diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 340528ad..d0d49189 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -13,10 +13,7 @@ import org.apache.commons.codec.binary.Base64; import java.nio.charset.StandardCharsets; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; /** @@ -360,6 +357,58 @@ public Builder withClaim(String name, List list) throws IllegalArgumentExcept return this; } + /** + * Add specific Claims to set as the Payload. If the provided map is null then + * nothing is changed. + *

+ * Accepted types are {@linkplain Map} and {@linkplain List} with basic types + * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, + * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. + * {@linkplain List}s can contain null elements. + *

+ * + *

+ * If any of the claims are invalid, none will be added. + *

+ * + * @param payloadClaims the values to use as Claims in the token's payload. + * @throws IllegalArgumentException if any of the claim keys or null, or if the values are not of a supported type. + * @return this same Builder instance. + */ + public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { + if (payloadClaims == null) { + return this; + } + + if (!validatePayload(payloadClaims)) { + throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + } + + // add claims only after validating all claims so as not to corrupt the claims map of this builder + for (Map.Entry entry : payloadClaims.entrySet()) { + addClaim(entry.getKey(), entry.getValue()); + } + + return this; + } + + private boolean validatePayload(Map payload) { + for (Map.Entry entry : payload.entrySet()) { + String key = entry.getKey(); + assertNonNull(key); + + Object value = entry.getValue(); + if (value instanceof List && !validateClaim((List) value)) { + return false; + } else if (value instanceof Map && !validateClaim((Map) value)) { + return false; + } else if (value != null && !isSupportedType(value)) { + return false; + } + } + return true; + } + private static boolean validateClaim(Map map) { // do not accept null values in maps for (Entry entry : map.entrySet()) { diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 2d9179ff..a1aea9b5 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -15,9 +15,7 @@ import java.security.interfaces.RSAPrivateKey; import java.util.*; -import static org.hamcrest.Matchers.anEmptyMap; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -717,4 +715,170 @@ public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception { .sign(Algorithm.HMAC256("secret")); } + @Test + public void withPayloadShouldAddBasicClaim() { + Map payload = new HashMap<>(); + payload.put("asd", 123); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry("asd", 123)); + } + + @Test + public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() { + String jwt = JWTCreator.init() + .withPayload(null) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, is("{}")); + } + + @Test + public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKey() { + Map payload = new HashMap<>(); + payload.put(PublicClaims.KEY_ID, "xyz"); + + String jwt = JWTCreator.init() + .withKeyId("abc") + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); + } + + @Test + public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { + Map payload = new HashMap<>(); + payload.put(PublicClaims.ISSUER, "xyz"); + + String jwt = JWTCreator.init() + .withPayload(payload) + .withIssuer("abc") + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.ISSUER, "abc")); + } + + @Test + public void shouldRemovePayloadIfTheValueIsNull() throws Exception { + String jwt = JWTCreator.init() + .withClaim("key", "stubValue") + .withPayload(Collections.singletonMap("key", (Map) null)) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + Map map = (Map) mapper.readValue(body, Map.class); + assertThat(map, anEmptyMap()); + } + + @Test + public void withPayloadShouldNotAllowCustomType() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + + Map payload = new HashMap<>(); + payload.put("entry", "value"); + payload.put("pojo", new UserPojo("name", 42)); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldAllowNullListItems() { + Map payload = new HashMap<>(); + payload.put("list", Arrays.asList("item1", null, "item2")); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry("list", Arrays.asList("item1", null, "item2"))); + } + + @Test + public void withPayloadShouldNotAllowListWithCustomType() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + + Map payload = new HashMap<>(); + payload.put("list", Arrays.asList("item1", new UserPojo("name", 42))); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldNotAllowMapWithCustomType() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + + Map payload = new HashMap<>(); + payload.put("entry", "value"); + payload.put("map", Collections.singletonMap("pojo", new UserPojo("name", 42))); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldAllowNestedSupportedTypes() { + /* + JWT: + { + "stringClaim": "string", + "intClaim": 41, + "listClaim": [ + 1, 2, { + "nestedObjKey": true + } + ], + "objClaim": { + "objKey": ["nestedList1", "nestedList2"] + } + } + */ + + List listClaim = Arrays.asList(1, 2, Collections.singletonMap("nestedObjKey", "nestedObjValue")); + Map mapClaim = new HashMap<>(); + mapClaim.put("objKey", Arrays.asList("nestedList1", true)); + + Map payload = new HashMap<>(); + payload.put("stringClaim", "string"); + payload.put("intClaim", 41); + payload.put("listClaim", listClaim); + payload.put("objClaim", mapClaim); + + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry("stringClaim", "string")); + assertThat(payloadJson, JsonMatcher.hasEntry("intClaim", 41)); + assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim)); + assertThat(payloadJson, JsonMatcher.hasEntry("objClaim", mapClaim)); + } } From fbbf140efc45854acbdc375fee497ff97fa4ff47 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 19 Feb 2021 14:21:50 -0600 Subject: [PATCH 120/293] Use wildcard instead of Object --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index d0d49189..0280af07 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -375,7 +375,7 @@ public Builder withClaim(String name, List list) throws IllegalArgumentExcept * @throws IllegalArgumentException if any of the claim keys or null, or if the values are not of a supported type. * @return this same Builder instance. */ - public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { + public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { if (payloadClaims == null) { return this; } @@ -385,15 +385,15 @@ public Builder withPayload(Map payloadClaims) throws IllegalArgu } // add claims only after validating all claims so as not to corrupt the claims map of this builder - for (Map.Entry entry : payloadClaims.entrySet()) { + for (Map.Entry entry : payloadClaims.entrySet()) { addClaim(entry.getKey(), entry.getValue()); } return this; } - private boolean validatePayload(Map payload) { - for (Map.Entry entry : payload.entrySet()) { + private boolean validatePayload(Map payload) { + for (Map.Entry entry : payload.entrySet()) { String key = entry.getKey(); assertNonNull(key); From 9987b98237f2988ea4f150e9fc1d91be6b31b582 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 24 Feb 2021 14:42:54 -0600 Subject: [PATCH 121/293] Update README to include HMAC key length recommendations --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 295dbd73..1bff22d1 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,10 @@ Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); > Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). +##### HMAC Key Length and Security + +When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recomendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. + #### Using a KeyProvider: By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: From 6b732d3931796e2789254301575f1fc0a8c231a7 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 25 Feb 2021 10:05:01 -0600 Subject: [PATCH 122/293] Add ability to run apiDiff for incompatible changes (#476) * Add ability to run apiDiff for incompatible changes * Add api diff to CI --- .circleci/config.yml | 57 +++++++++++++++++++++++++++++++++++--------- lib/build.gradle | 1 + settings.gradle | 2 +- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9bb01639..78c9dab4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,7 @@ -version: 2 -jobs: - build: - docker: - - image: openjdk:8-jdk +version: 2.1 + +commands: + checkout-and-build: steps: - checkout - run: chmod +x gradlew @@ -12,17 +11,53 @@ jobs: - v1-dependencies-{{ checksum "build.gradle" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - # run tests! - - run: ./gradlew clean check jacocoTestReport --continue --console=plain - - run: - name: Upload Coverage - when: on_success - command: bash <(curl -s https://codecov.io/bash) -Z -C $CIRCLE_SHA1 + - run: ./gradlew clean build - save_cache: paths: - ~/.m2 key: v1-dependencies-{{ checksum "build.gradle" }} + run-tests: + steps: + - run: ./gradlew check jacocoTestReport --continue --console=plain + - run: + name: Upload Coverage + when: on_success + command: bash <(curl -s https://codecov.io/bash) -Z -C $CIRCLE_SHA1 + run-api-diff: + steps: + # run apiDiff task + - run: ./gradlew apiDiff + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.txt + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.html + +jobs: + build: + docker: + - image: openjdk:8-jdk + steps: + - checkout-and-build + - run-tests + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + TERM: dumb + api-diff: + docker: + - image: openjdk:8-jdk + steps: + - checkout-and-build + - run-api-diff environment: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb + +workflows: + build-and-test: + jobs: + - build + api-diff: + jobs: + - api-diff diff --git a/lib/build.gradle b/lib/build.gradle index fd920e2c..209e90ce 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -12,6 +12,7 @@ oss { repository "java-jwt" organization "auth0" description "Java implementation of JSON Web Token (JWT)" + baselineCompareVersion "3.13.0" developers { auth0 { diff --git a/settings.gradle b/settings.gradle index d3c7fb14..2170ea25 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ pluginManagement { } plugins { id 'com.jfrog.bintray' version '1.8.5' - id 'com.auth0.gradle.oss-library.java' version '0.12.1' + id 'com.auth0.gradle.oss-library.java' version '0.14.0' } } From fb51a2f147cfa95cbb64d7990b775cf11a64e6d6 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 26 Feb 2021 12:22:01 -0600 Subject: [PATCH 123/293] Release 3.14.0 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 887528ab..92116fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.14.0](https://github.com/auth0/java-jwt/tree/3.14.0) (2021-02-26) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.13.0...3.14.0) + +**Added** +- Add withPayload to JWTCreator.Builder [\#475](https://github.com/auth0/java-jwt/pull/475) ([jimmyjames](https://github.com/jimmyjames)) + ## [3.13.0](https://github.com/auth0/java-jwt/tree/3.13.0) (2021-02-05) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.12.1...3.13.0) diff --git a/README.md b/README.md index 1bff22d1..65e033fa 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.13.0 + 3.14.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.13.0' +implementation 'com.auth0:java-jwt:3.14.0' ``` ## Available Algorithms From 7dea6ac54d5b5b8822a9f3ee41cc4666e250cc27 Mon Sep 17 00:00:00 2001 From: XakepSDK Date: Mon, 8 Mar 2021 15:56:13 +0000 Subject: [PATCH 124/293] Move form commons-codec Base64 to j.u.Base64 (#478) * Use Java Base64 encoding and decoding fix last usages of commons-codec Remove codec usages Remove commons-codec * Throw on invalid Base64 encoded tokens * Fix broken tests that became broken after java.util.Base64 transition Co-authored-by: jimmyjames Co-authored-by: Xakep_SDK --- lib/build.gradle | 1 - .../main/java/com/auth0/jwt/JWTCreator.java | 9 ++-- .../main/java/com/auth0/jwt/JWTDecoder.java | 8 +-- .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 7 ++- .../auth0/jwt/algorithms/HMACAlgorithm.java | 7 ++- .../auth0/jwt/algorithms/NoneAlgorithm.java | 14 +++-- .../auth0/jwt/algorithms/RSAAlgorithm.java | 7 ++- .../java/com/auth0/jwt/JWTCreatorTest.java | 54 +++++++++---------- .../java/com/auth0/jwt/JWTDecoderTest.java | 27 ++++++++-- lib/src/test/java/com/auth0/jwt/JWTTest.java | 22 ++++---- .../jwt/algorithms/CryptoTestHelper.java | 11 ++-- .../jwt/algorithms/ECDSAAlgorithmTest.java | 53 ++++++++++-------- .../ECDSABouncyCastleProviderTests.java | 45 ++++++++-------- .../jwt/algorithms/HMACAlgorithmTest.java | 16 +++++- .../jwt/algorithms/NoneAlgorithmTest.java | 13 ++++- .../jwt/algorithms/RSAAlgorithmTest.java | 9 ++++ 16 files changed, 183 insertions(+), 120 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 209e90ce..e0894e3d 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -43,7 +43,6 @@ compileJava { dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0' - implementation 'commons-codec:commons-codec:1.14' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' testImplementation 'net.jodah:concurrentunit:0.4.3' diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 0280af07..26df771f 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; -import org.apache.commons.codec.binary.Base64; import java.nio.charset.StandardCharsets; import java.util.*; @@ -27,7 +26,7 @@ public final class JWTCreator { private final Algorithm algorithm; private final String headerJson; private final String payloadJson; - + private static final ObjectMapper mapper; private static final SimpleModule module; @@ -492,11 +491,11 @@ private void addClaim(String name, Object value) { } private String sign() throws SignatureGenerationException { - String header = Base64.encodeBase64URLSafeString(headerJson.getBytes(StandardCharsets.UTF_8)); - String payload = Base64.encodeBase64URLSafeString(payloadJson.getBytes(StandardCharsets.UTF_8)); + String header = Base64.getUrlEncoder().withoutPadding().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8)); + String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8)); byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); - String signature = Base64.encodeBase64URLSafeString((signatureBytes)); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString((signatureBytes)); return String.format("%s.%s.%s", header, payload, signature); } diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index 445bf95d..71acbe59 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -6,10 +6,10 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Header; import com.auth0.jwt.interfaces.Payload; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.binary.StringUtils; import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Date; import java.util.List; import java.util.Map; @@ -37,8 +37,8 @@ final class JWTDecoder implements DecodedJWT, Serializable { String headerJson; String payloadJson; try { - headerJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[0])); - payloadJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[1])); + headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); } catch (NullPointerException e) { throw new JWTDecodeException("The UTF-8 Charset isn't initialized.", e); } catch (IllegalArgumentException e){ diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 6d065c9c..c2d08a15 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -4,13 +4,13 @@ import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.ECDSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.util.Base64; /** * Subclass representing an Elliptic Curve signing algorithm @@ -40,9 +40,8 @@ class ECDSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { + byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); ECPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId()); if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); @@ -52,7 +51,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException | IllegalArgumentException e) { throw new SignatureVerificationException(this, e); } } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 596f907e..c5792457 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -3,12 +3,12 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.Base64; /** * Subclass representing an Hash-based MAC signing algorithm @@ -48,14 +48,13 @@ static byte[] getSecretBytes(String secret) throws IllegalArgumentException { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { + byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); boolean valid = crypto.verifySignatureFor(getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } - } catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException e) { + } catch (IllegalStateException | InvalidKeyException | NoSuchAlgorithmException | IllegalArgumentException e) { throw new SignatureVerificationException(this, e); } } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java index f70b72b2..076a6b94 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java @@ -3,7 +3,8 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; + +import java.util.Base64; class NoneAlgorithm extends Algorithm { @@ -13,9 +14,14 @@ class NoneAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - if (signatureBytes.length > 0) { - throw new SignatureVerificationException(this); + try { + byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); + + if (signatureBytes.length > 0) { + throw new SignatureVerificationException(this); + } + } catch (IllegalArgumentException e) { + throw new SignatureVerificationException(this, e); } } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index a61fc97e..31773a09 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -4,7 +4,6 @@ import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.RSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; @@ -12,6 +11,7 @@ import java.security.SignatureException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.Base64; /** * Subclass representing an RSA signing algorithm @@ -39,9 +39,8 @@ class RSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { + byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); RSAPublicKey publicKey = keyProvider.getPublicKeyById(jwt.getKeyId()); if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); @@ -50,7 +49,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException | IllegalStateException e) { throw new SignatureVerificationException(this, e); } } diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index a1aea9b5..eabc2f31 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -5,7 +5,7 @@ import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.codec.binary.Base64; + import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -49,7 +49,7 @@ public void shouldAddHeaderClaim() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("asd", 123)); } @@ -74,7 +74,7 @@ public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() throws assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); } @@ -90,7 +90,7 @@ public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() throws Exce assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "abc")); } @@ -107,7 +107,7 @@ public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.isNotPresent(PublicClaims.KEY_ID)); assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet")); } @@ -120,7 +120,7 @@ public void shouldAddKeyId() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "56a8bd44da435300010000015f5ed")); } @@ -136,7 +136,7 @@ public void shouldAddKeyIdIfAvailableFromRSAAlgorithms() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -153,7 +153,7 @@ public void shouldNotOverwriteKeyIdIfAddedFromRSAAlgorithms() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -169,7 +169,7 @@ public void shouldAddKeyIdIfAvailableFromECDSAKAlgorithms() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -186,7 +186,7 @@ public void shouldNotOverwriteKeyIdIfAddedFromECDSAKAlgorithms() throws Exceptio assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -202,7 +202,7 @@ public void shouldAddKeyIdIfAvailableFromECDSAAlgorithms() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -219,7 +219,7 @@ public void shouldNotOverwriteKeyIdIfAddedFromECDSAAlgorithms() throws Exception assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); } @@ -319,7 +319,7 @@ public void shouldSetCorrectAlgorithmInTheHeader() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS256")); } @@ -330,7 +330,7 @@ public void shouldSetDefaultTypeInTheHeader() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); } @@ -343,7 +343,7 @@ public void shouldSetCustomTypeInTheHeader() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("typ", "passport")); } @@ -521,7 +521,7 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); Map map = (Map) mapper.readValue(body, Map.class).get("data"); @@ -574,8 +574,8 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - - String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); List list = (List) mapper.readValue(body, Map.class).get("data"); @@ -618,7 +618,7 @@ public void shouldAcceptCustomClaimWithNullMapAndRemoveClaim() throws Exception assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); Map map = (Map) mapper.readValue(body, Map.class); assertThat(map, anEmptyMap()); @@ -635,7 +635,7 @@ public void shouldAcceptCustomClaimWithNullListAndRemoveClaim() throws Exception assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); Map map = (Map) mapper.readValue(body, Map.class); assertThat(map, anEmptyMap()); @@ -725,7 +725,7 @@ public void withPayloadShouldAddBasicClaim() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, JsonMatcher.hasEntry("asd", 123)); } @@ -737,7 +737,7 @@ public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, is("{}")); } @@ -753,7 +753,7 @@ public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKe assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); } @@ -769,7 +769,7 @@ public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.ISSUER, "abc")); } @@ -783,7 +783,7 @@ public void shouldRemovePayloadIfTheValueIsNull() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String body = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); Map map = (Map) mapper.readValue(body, Map.class); assertThat(map, anEmptyMap()); @@ -812,7 +812,7 @@ public void withPayloadShouldAllowNullListItems() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, JsonMatcher.hasEntry("list", Arrays.asList("item1", null, "item2"))); } @@ -875,7 +875,7 @@ public void withPayloadShouldAllowNestedSupportedTypes() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - String payloadJson = new String(Base64.decodeBase64(parts[1]), StandardCharsets.UTF_8); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); assertThat(payloadJson, JsonMatcher.hasEntry("stringClaim", "string")); assertThat(payloadJson, JsonMatcher.hasEntry("intClaim", 41)); assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim)); diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index efd1b398..5cfe069e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -4,7 +4,6 @@ import com.auth0.jwt.impl.NullClaim; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.core.IsCollectionContaining; import org.junit.Assert; @@ -14,6 +13,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Date; import java.util.Map; @@ -35,7 +35,8 @@ public void getSubject() throws Exception { @Test public void shouldThrowIfTheContentIsNotProperlyEncoded() throws Exception { exception.expect(JWTDecodeException.class); - exception.expectMessage("The input is not a valid base 64 encoded string."); + exception.expectMessage(startsWith("The string '")); + exception.expectMessage(endsWith("' doesn't have a valid JSON format.")); JWT.decode("eyJ0eXAiOiJKV1QiLCJhbGciO-corrupted.eyJ0ZXN0IjoxMjN9.sLtFC2rLAzN0-UJ13OLQX6ezNptAQzespaOGwCnpqk"); } @@ -71,6 +72,24 @@ public void shouldThrowIfHeaderHasInvalidJSONFormat() throws Exception { customJWT(invalidJson, validJson, "signature"); } + @Test + public void shouldThrowWhenHeaderNotValidBase64() { + exception.expect(JWTDecodeException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + String jwt = "eyJhbGciOiJub25l+IiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9.Ox-WRXRaGAuWt2KfPvWiGcCrPqZtbp_4OnQzZXaTfss"; + JWT.decode(jwt); + } + + @Test + public void shouldThrowWhenPayloadNotValidBase64() { + exception.expect(JWTDecodeException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRo+MCJ9.Ox-WRXRaGAuWt2KfPvWiGcCrPqZtbp_4OnQzZXaTfss"; + JWT.decode(jwt); + } + // Parts @Test @@ -328,8 +347,8 @@ public void shouldSerializeAndDeserialize() throws Exception { //Helper Methods private DecodedJWT customJWT(String jsonHeader, String jsonPayload, String signature) { - String header = Base64.encodeBase64URLSafeString(jsonHeader.getBytes(StandardCharsets.UTF_8)); - String body = Base64.encodeBase64URLSafeString(jsonPayload.getBytes(StandardCharsets.UTF_8)); + String header = Base64.getUrlEncoder().withoutPadding().encodeToString(jsonHeader.getBytes(StandardCharsets.UTF_8)); + String body = Base64.getUrlEncoder().withoutPadding().encodeToString(jsonPayload.getBytes(StandardCharsets.UTF_8)); return JWT.decode(String.format("%s.%s.%s", header, body, signature)); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index b778be13..bcf62e05 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -3,7 +3,6 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.Clock; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.core.IsCollectionContaining; import org.junit.Rule; @@ -15,6 +14,7 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; +import java.util.Base64; import java.util.Date; import static org.hamcrest.Matchers.*; @@ -393,7 +393,7 @@ public void shouldCreateAnEmptyHMAC256SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS256")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -409,7 +409,7 @@ public void shouldCreateAnEmptyHMAC384SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS384")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -425,7 +425,7 @@ public void shouldCreateAnEmptyHMAC512SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS512")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -441,7 +441,7 @@ public void shouldCreateAnEmptyRSA256SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "RS256")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -457,7 +457,7 @@ public void shouldCreateAnEmptyRSA384SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "RS384")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -473,7 +473,7 @@ public void shouldCreateAnEmptyRSA512SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "RS512")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -489,7 +489,7 @@ public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES256")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -508,7 +508,7 @@ public void shouldCreateAnEmptyECDSA256KSignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES256K")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -524,7 +524,7 @@ public void shouldCreateAnEmptyECDSA384SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES384")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); @@ -540,7 +540,7 @@ public void shouldCreateAnEmptyECDSA512SignedToken() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); assertThat(headerJson, JsonMatcher.hasEntry("alg", "ES512")); assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); assertThat(parts[1], is("e30")); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java index f977e42a..25b08d70 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java @@ -1,14 +1,13 @@ package com.auth0.jwt.algorithms; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.codec.binary.Base64; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; public abstract class CryptoTestHelper { @@ -16,7 +15,7 @@ public abstract class CryptoTestHelper { public static String asJWT(Algorithm algorithm, String header, String payload) { byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); + String jwtSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes); return String.format("%s.%s.%s", header, payload, jwtSignature); } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 7f7b9c94..39c9dabf 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -4,8 +4,6 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.ECDSAKeyProvider; - -import org.apache.commons.codec.binary.Base64; import org.hamcrest.Matchers; import org.hamcrest.collection.IsIn; import org.junit.Assert; @@ -20,13 +18,15 @@ import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.util.Arrays; +import java.util.Base64; import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; import static org.mockito.ArgumentMatchers.any; @@ -156,7 +156,7 @@ public void shouldFailECDSA256VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[63]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -169,7 +169,7 @@ public void shouldFailECDSA256VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[64]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -183,7 +183,7 @@ public void shouldFailECDSA256VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[64]; bytes[0] = 0x30; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -260,8 +260,8 @@ public void shouldFailECDSA256KVerificationWhenUsingPrivateKey() throws Exceptio public void shouldFailECDSA256KVerificationOnInvalidJOSESignatureLength() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); - exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + exception.expectCause(isA(IllegalArgumentException.class)); + exception.expectCause(hasMessage(is("Last unit does not have enough valid bits"))); String jwt = ES256K_JWT.substring(0,ES256K_JWT.length()-1); Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); @@ -275,7 +275,7 @@ public void shouldFailECDSA256KVerificationOnInvalidJOSESignature() throws Excep byte[] bytes = new byte[64]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); algorithm.verify(JWT.decode(jwt)); @@ -289,7 +289,7 @@ public void shouldFailECDSA256KVerificationOnInvalidDERSignature() throws Except byte[] bytes = new byte[64]; bytes[0] = 0x30; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); algorithm.verify(JWT.decode(jwt)); @@ -386,7 +386,7 @@ public void shouldFailECDSA384VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[95]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -399,7 +399,7 @@ public void shouldFailECDSA384VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[96]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -413,7 +413,7 @@ public void shouldFailECDSA384VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[96]; new SecureRandom().nextBytes(bytes); bytes[0] = 0x30; - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -511,7 +511,7 @@ public void shouldFailECDSA512VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[131]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -524,7 +524,7 @@ public void shouldFailECDSA512VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[132]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -538,7 +538,7 @@ public void shouldFailECDSA512VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[132]; new SecureRandom().nextBytes(bytes); bytes[0] = 0x30; - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -553,7 +553,7 @@ public void shouldFailJOSEToDERConversionOnInvalidJOSESignatureLength() throws E byte[] bytes = new byte[256]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); @@ -617,7 +617,18 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception algorithm.verify(JWT.decode(jwt)); } - //Sign + @Test + public void shouldThrowWhenSignatureNotValidBase64() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4+EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + Algorithm algorithm = Algorithm.ECDSA512(key); + algorithm.verify(JWT.decode(jwt)); + } + + //Sign private static final String ES256Header = "eyJhbGciOiJFUzI1NiJ9"; private static final String ES384Header = "eyJhbGciOiJFUzM4NCJ9"; private static final String ES512Header = "eyJhbGciOiJFUzUxMiJ9"; @@ -643,7 +654,7 @@ public void shouldDoECDSA256Signing() throws Exception { public void shouldDoECDSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); byte[] signatureBytes = algorithm.sign(ES256HeaderBytes, auth0IssPayloadBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); + String jwtSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes); String jwt = String.format("%s.%s.%s", ES256Header, auth0IssPayload, jwtSignature); assertSignaturePresent(jwt); @@ -1267,12 +1278,12 @@ public void shouldBeEqualSignatureMethodDecodeResults() throws Exception { bout.write('.'); bout.write(payloadBytes); - String jwtSignature1 = Base64.encodeBase64URLSafeString(algorithm.sign(bout.toByteArray())); + String jwtSignature1 = Base64.getUrlEncoder().withoutPadding().encodeToString(algorithm.sign(bout.toByteArray())); String jwt1 = String.format("%s.%s.%s", header, payload, jwtSignature1); algorithm.verify(JWT.decode(jwt1)); - String jwtSignature2 = Base64.encodeBase64URLSafeString(algorithm.sign(headerBytes, payloadBytes)); + String jwtSignature2 = Base64.getUrlEncoder().withoutPadding().encodeToString(algorithm.sign(headerBytes, payloadBytes)); String jwt2 = String.format("%s.%s.%s", header, payload, jwtSignature2); algorithm.verify(JWT.decode(jwt2)); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java index dbecae1e..68fe6e50 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -1,11 +1,9 @@ package com.auth0.jwt.algorithms; -import static com.auth0.jwt.algorithms.CryptoTestHelper.*; import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.ECDSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -18,9 +16,12 @@ import java.security.interfaces.ECKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.util.Base64; import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; +import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; +import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; import static com.auth0.jwt.algorithms.ECDSAAlgorithmTest.*; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.*; @@ -150,8 +151,8 @@ public void shouldFailECDSA256KVerificationWhenUsingPrivateKey() throws Exceptio public void shouldFailECDSA256KVerificationOnInvalidJOSESignatureLength() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); - exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + exception.expectCause(isA(IllegalArgumentException.class)); + exception.expectCause(hasMessage(is("Last unit does not have enough valid bits"))); String jwt = ES256K_JWT.substring(0,ES256K_JWT.length()-1); Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); @@ -165,7 +166,7 @@ public void shouldFailECDSA256KVerificationOnInvalidJOSESignature() throws Excep byte[] bytes = new byte[64]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); algorithm.verify(JWT.decode(jwt)); @@ -179,7 +180,7 @@ public void shouldFailECDSA256KVerificationOnInvalidDERSignature() throws Except byte[] bytes = new byte[64]; bytes[0] = 0x30; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NksifQo.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256K((ECPublicKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256K, "EC"), null); algorithm.verify(JWT.decode(jwt)); @@ -200,7 +201,7 @@ public void shouldThrowOnECDSA256VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); Algorithm algorithm = Algorithm.ECDSA256(key); algorithm.verify(JWT.decode(jwt)); @@ -220,7 +221,7 @@ public void shouldThrowOnECDSA256VerificationWithDERSignatureWithBothKeys() thro exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -277,7 +278,7 @@ public void shouldFailECDSA256VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[63]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -290,7 +291,7 @@ public void shouldFailECDSA256VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[64]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -304,7 +305,7 @@ public void shouldFailECDSA256VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[64]; bytes[0] = 0x30; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -325,7 +326,7 @@ public void shouldThrowOnECDSA384VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UtccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); Algorithm algorithm = Algorithm.ECDSA384(key); algorithm.verify(JWT.decode(jwt)); @@ -345,7 +346,7 @@ public void shouldThrowOnECDSA384VerificationWithDERSignatureWithBothKeys() thro exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UtccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -402,7 +403,7 @@ public void shouldFailECDSA384VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[95]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -415,7 +416,7 @@ public void shouldFailECDSA384VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[96]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -429,7 +430,7 @@ public void shouldFailECDSA384VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[96]; new SecureRandom().nextBytes(bytes); bytes[0] = 0x30; - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -450,7 +451,7 @@ public void shouldThrowOnECDSA512VerificationWithDERSignature() throws Exception exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); Algorithm algorithm = Algorithm.ECDSA512(key); algorithm.verify(JWT.decode(jwt)); @@ -470,7 +471,7 @@ public void shouldThrowECDSA512VerificationWithDERSignatureWithBothKeys() throws exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -527,7 +528,7 @@ public void shouldFailECDSA512VerificationOnInvalidJOSESignatureLength() throws byte[] bytes = new byte[131]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -540,7 +541,7 @@ public void shouldFailECDSA512VerificationOnInvalidJOSESignature() throws Except byte[] bytes = new byte[132]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -554,7 +555,7 @@ public void shouldFailECDSA512VerificationOnInvalidDERSignature() throws Excepti byte[] bytes = new byte[132]; new SecureRandom().nextBytes(bytes); bytes[0] = 0x30; - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); algorithm.verify(JWT.decode(jwt)); @@ -569,7 +570,7 @@ public void shouldFailJOSEToDERConversionOnInvalidJOSESignatureLength() throws E byte[] bytes = new byte[256]; new SecureRandom().nextBytes(bytes); - String signature = Base64.encodeBase64URLSafeString(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 38aaf301..fd5aed71 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -293,4 +293,18 @@ public void shouldBeEqualSignatureMethodResults() throws Exception { assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); } -} + + @Test + public void shouldThrowWhenSignatureNotValidBase64() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + CryptoHelper crypto = mock(CryptoHelper.class); + when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(String.class), any(String.class), any(byte[].class))) + .thenThrow(NoSuchAlgorithmException.class); + + Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); + String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWm+i903JuUoDRZDBPB7HwkS4nVyWH1M"; + algorithm.verify(JWT.decode(jwt)); + } +} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java index f6e72b84..9c528f68 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java @@ -7,8 +7,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; public class NoneAlgorithmTest { @@ -45,4 +44,14 @@ public void shouldFailNoneVerificationWhenSignatureIsPresent() throws Exception public void shouldReturnNullSigningKeyId() throws Exception { assertThat(Algorithm.none().getSigningKeyId(), is(nullValue())); } + + @Test + public void shouldThrowWhenSignatureNotValidBase64() { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9.Ox-WRXRaGAuWt2KfPvW+iGcCrPqZtbp_4OnQzZXaTfss"; + Algorithm algorithm = Algorithm.none(); + algorithm.verify(JWT.decode(jwt)); + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java index b24365e3..a4902781 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -569,4 +569,13 @@ public void shouldFailOnRSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyI algorithm.sign(new byte[0]); } + @Test + public void shouldThrowWhenSignatureNotValidBase64() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(IllegalArgumentException.class)); + + String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNu+LAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; + Algorithm algorithm = Algorithm.RSA256((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + algorithm.verify(JWT.decode(jwt)); + } } \ No newline at end of file From 523b2d6cc091ab0026bef4ad2b72f7f502babd68 Mon Sep 17 00:00:00 2001 From: Josiah Boning Date: Thu, 25 Mar 2021 22:35:10 -0700 Subject: [PATCH 125/293] Update Java version in README.md Java 8 has been required since #457. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65e033fa..871bdcfa 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.or If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. -> This library currently supports Java 7. Beginning soon, it will require Java 8 as the minimum supported Java version. See [this issue](https://github.com/auth0/java-jwt/issues/457) for additional information and timelines. +> This library requires Java 8 or higher. The last version that supported Java 7 was 3.11.0. ## Installation From 603e2b738f185be6a9a4036c2c27634d5d750029 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 31 Mar 2021 01:33:13 +0200 Subject: [PATCH 126/293] Improve README formatting --- README.md | 98 +++++++++++++++++++++++++++---------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 871bdcfa..b86ad957 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) -[![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) +[![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org) [![Javadoc](https://javadoc.io/badge2/com.auth0/java-jwt/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). @@ -122,31 +122,31 @@ You'll first need to create a `JWTCreator` instance by calling `JWT.create()`. U * Example using `HS256` -```java -try { - Algorithm algorithm = Algorithm.HMAC256("secret"); - String token = JWT.create() - .withIssuer("auth0") - .sign(algorithm); -} catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. -} -``` + ```java + try { + Algorithm algorithm = Algorithm.HMAC256("secret"); + String token = JWT.create() + .withIssuer("auth0") + .sign(algorithm); + } catch (JWTCreationException exception){ + //Invalid Signing configuration / Couldn't convert Claims. + } + ``` * Example using `RS256` -```java -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance -try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); - String token = JWT.create() - .withIssuer("auth0") - .sign(algorithm); -} catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. -} -``` + ```java + RSAPublicKey publicKey = //Get the key instance + RSAPrivateKey privateKey = //Get the key instance + try { + Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + String token = JWT.create() + .withIssuer("auth0") + .sign(algorithm); + } catch (JWTCreationException exception){ + //Invalid Signing configuration / Couldn't convert Claims. + } + ``` If a Claim couldn't be converted to JSON or the Key used in the signing process was invalid a `JWTCreationException` will raise. @@ -157,35 +157,35 @@ You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` * Example using `HS256` -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -try { - Algorithm algorithm = Algorithm.HMAC256("secret"); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); -} catch (JWTVerificationException exception){ - //Invalid signature/claims -} -``` + ```java + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; + try { + Algorithm algorithm = Algorithm.HMAC256("secret"); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer("auth0") + .build(); //Reusable verifier instance + DecodedJWT jwt = verifier.verify(token); + } catch (JWTVerificationException exception){ + //Invalid signature/claims + } + ``` * Example using `RS256` -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance -try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); -} catch (JWTVerificationException exception){ - //Invalid signature/claims -} -``` + ```java + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; + RSAPublicKey publicKey = //Get the key instance + RSAPrivateKey privateKey = //Get the key instance + try { + Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer("auth0") + .build(); //Reusable verifier instance + DecodedJWT jwt = verifier.verify(token); + } catch (JWTVerificationException exception){ + //Invalid signature/claims + } + ``` If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise. From 35570e7ef3c036fc3e13a2fb8372a095f73d9f94 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Tue, 9 Mar 2021 19:35:05 -0600 Subject: [PATCH 127/293] remove jcenter --- build.gradle | 2 +- settings.gradle | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index bf6dfe4f..c938b200 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,6 @@ allprojects { group = 'com.auth0' repositories { - jcenter() + mavenCentral() } } diff --git a/settings.gradle b/settings.gradle index 2170ea25..962399d3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,6 @@ pluginManagement { repositories { gradlePluginPortal() - jcenter() } plugins { id 'com.jfrog.bintray' version '1.8.5' From 6ce1991f9b35a69c1e49dba2b12909f1fc86fb4c Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 5 Apr 2021 12:03:53 -0500 Subject: [PATCH 128/293] Release 3.15.0 --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92116fa2..3cf74f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [3.15.0](https://github.com/auth0/java-jwt/tree/3.15.0) (2021-04-05) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.14.0...3.15.0) + +**Changed** +- Remove jcenter [\#482](https://github.com/auth0/java-jwt/pull/482) ([jimmyjames](https://github.com/jimmyjames)) +- Move form commons-codec Base64 to j.u.Base64 [\#478](https://github.com/auth0/java-jwt/pull/478) ([XakepSDK](https://github.com/XakepSDK)) + ## [3.14.0](https://github.com/auth0/java-jwt/tree/3.14.0) (2021-02-26) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.13.0...3.14.0) diff --git a/README.md b/README.md index b86ad957..2c26f951 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.14.0 + 3.15.0 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.14.0' +implementation 'com.auth0:java-jwt:3.15.0' ``` ## Available Algorithms From 75272c3343d7e058843e164abc29e3f42a202409 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 5 Apr 2021 12:20:40 -0500 Subject: [PATCH 129/293] Update OSS plugin version --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 962399d3..7faf41ff 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,7 +4,7 @@ pluginManagement { } plugins { id 'com.jfrog.bintray' version '1.8.5' - id 'com.auth0.gradle.oss-library.java' version '0.14.0' + id 'com.auth0.gradle.oss-library.java' version '0.15.1' } } From 6ded84d9c490b06d9f9f6be09c34f441b2317798 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Thu, 8 Apr 2021 18:04:47 +0200 Subject: [PATCH 130/293] delete duplicated template --- .github/ISSUE_TEMPLATE/report_a_bug.md | 55 -------------------------- 1 file changed, 55 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/report_a_bug.md diff --git a/.github/ISSUE_TEMPLATE/report_a_bug.md b/.github/ISSUE_TEMPLATE/report_a_bug.md deleted file mode 100644 index 50b9fa7e..00000000 --- a/.github/ISSUE_TEMPLATE/report_a_bug.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: Report a bug -about: Have you found a bug or issue? Create a bug report for this SDK -title: '' -labels: bug report -assignees: '' ---- - - - -### Describe the problem - - - -### What was the expected behavior? - - - -### Reproduction - - -- Step 1.. -- Step 2.. -- ... - -### Environment - - - -- **Version of this library used:** -- **Which framework are you using, if applicable:** -- **Other modules/plugins/libraries that might be involved:** -- **Any other relevant information you think would be useful:** \ No newline at end of file From a643cebdc713acdcf790b7f17f57c4aea2211987 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 25 Apr 2021 18:04:30 +0200 Subject: [PATCH 131/293] Add Eclipse files to .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 5ab9b34a..b5766efd 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,11 @@ crashlytics.properties crashlytics-build.properties fabric.properties +# Eclipse IDE +.classpath +.project +.settings/ + ### Java template *.class From 85bf1534586c8e8858b7bdf1a5dbd5f200574f52 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 25 Apr 2021 18:14:51 +0200 Subject: [PATCH 132/293] Add package-info.java for internal `impl` package --- lib/src/main/java/com/auth0/jwt/impl/NullClaim.java | 2 +- lib/src/main/java/com/auth0/jwt/impl/package-info.java | 7 +++++++ lib/src/main/java/com/auth0/jwt/interfaces/Header.java | 3 ++- lib/src/main/java/com/auth0/jwt/interfaces/Payload.java | 3 ++- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 lib/src/main/java/com/auth0/jwt/impl/package-info.java diff --git a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java index 8d10ca10..2e9a38c2 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java @@ -8,7 +8,7 @@ import java.util.Map; /** - * The {@link NullClaim} class is a Claim implementation that returns null when any of it's methods it's called. + * The {@code NullClaim} class is a Claim implementation that returns null when any of it's methods is called. */ public class NullClaim implements Claim { @Override diff --git a/lib/src/main/java/com/auth0/jwt/impl/package-info.java b/lib/src/main/java/com/auth0/jwt/impl/package-info.java new file mode 100644 index 00000000..334ccb8a --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/package-info.java @@ -0,0 +1,7 @@ +/** + * Contains parts of the internal implementation of this library. + * + *

Do not use any of the classes in this package. They might be removed + * or changed at any point without prior warning. + */ +package com.auth0.jwt.impl; diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Header.java b/lib/src/main/java/com/auth0/jwt/interfaces/Header.java index 0a83d1ce..030643d0 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Header.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Header.java @@ -34,7 +34,8 @@ public interface Header { String getKeyId(); /** - * Get a Private Claim given it's name. If the Claim wasn't specified in the Header, a NullClaim will be returned. + * Get a Private Claim given it's name. If the Claim wasn't specified in the Header, a 'null claim' will be + * returned. All of the methods of that claim will return {@code null}. * * @param name the name of the Claim to retrieve. * @return a non-null Claim. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java index 0f639ab9..b999ad43 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java @@ -59,7 +59,8 @@ public interface Payload { String getId(); /** - * Get a Claim given it's name. If the Claim wasn't specified in the Payload, a NullClaim will be returned. + * Get a Claim given it's name. If the Claim wasn't specified in the Payload, a 'null claim' + * will be returned. All of the methods of that claim will return {@code null}. * * @param name the name of the Claim to retrieve. * @return a non-null Claim. From e745182d847eb162a96f3967265aba9b916c842d Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 31 Mar 2021 02:01:11 +0200 Subject: [PATCH 133/293] Use JDK 16 for javadoc generation This allows using the latest javadoc features (such as a search bar) without having to the change the lowest Java version supported by the project. --- lib/build.gradle | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index e0894e3d..e394a18e 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -30,15 +30,30 @@ oss { } } +def javaVersion = 8 java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(javaVersion) } } compileJava { - sourceCompatibility '1.8' - targetCompatibility '1.8' + sourceCompatibility "$javaVersion" + targetCompatibility "$javaVersion" +} + +// Use latest JDK for javadoc generation +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + languageVersion = JavaLanguageVersion.of(16) + } +} + +javadoc { + // Specify Java version this project uses + // Otherwise would use version of javadoc toolchain by default which could + // cause compilation error mismatch between compileJava and javadoc creation + options.addStringOption('-release', "$javaVersion") } dependencies { From 64cb404a816cc9f5f8523f7b2a348547cf024c80 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 25 Apr 2021 18:48:13 +0200 Subject: [PATCH 134/293] Exclude internal `impl` package from javadoc --- lib/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/build.gradle b/lib/build.gradle index e394a18e..851ed055 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -50,6 +50,8 @@ tasks.withType(Javadoc).configureEach { } javadoc { + // Exclude internal implementation package from javadoc + excludes = ['com/auth0/jwt/impl'] // Specify Java version this project uses // Otherwise would use version of javadoc toolchain by default which could // cause compilation error mismatch between compileJava and javadoc creation From 7d1035cf79a21d1fc3c068b527bd230f07ac8ca4 Mon Sep 17 00:00:00 2001 From: Jeffrey Swan <61218880+plan-do-break-fix@users.noreply.github.com> Date: Thu, 29 Apr 2021 09:04:14 -0500 Subject: [PATCH 135/293] fix(docs) - corrects typo in project documentation (#494) Co-authored-by: Jim Anderson --- .github/ISSUE_TEMPLATE/report-a-bug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/report-a-bug.md b/.github/ISSUE_TEMPLATE/report-a-bug.md index 60505a3f..e5cf8c46 100644 --- a/.github/ISSUE_TEMPLATE/report-a-bug.md +++ b/.github/ISSUE_TEMPLATE/report-a-bug.md @@ -29,7 +29,7 @@ By submitting an Issue to this repository, you agree to the terms within the Aut ### Reproduction + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index a672afaf..001963bd 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -2,6 +2,12 @@ plugins { id 'java' id 'jacoco' id 'com.auth0.gradle.oss-library.java' + id 'checkstyle' +} + +checkstyle { + toolVersion '10.0' + checkstyleTest.enabled = false //We are disabling lint checks for tests } logger.lifecycle("Using version ${version} for ${group}.${name}") diff --git a/lib/src/main/java/com/auth0/jwt/JWT.java b/lib/src/main/java/com/auth0/jwt/JWT.java index ca05deff..9d719d1e 100644 --- a/lib/src/main/java/com/auth0/jwt/JWT.java +++ b/lib/src/main/java/com/auth0/jwt/JWT.java @@ -6,6 +6,9 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; +/** + * Exposes all the JWT functionalities. + */ @SuppressWarnings("WeakerAccess") public class JWT { @@ -22,11 +25,13 @@ public JWT() { /** * Decode a given Json Web Token. *

- * Note that this method doesn't verify the token's signature! Use it only if you trust the token or you already verified it. + * Note that this method doesn't verify the token's signature! + * Use it only if you trust the token or you already verified it. * * @param token with jwt format as string. * @return a decoded JWT. - * @throws JWTDecodeException if any part of the token contained an invalid jwt or JSON format of each of the jwt parts. + * @throws JWTDecodeException if any part of the token contained an invalid jwt + * or JSON format of each of the jwt parts. */ public DecodedJWT decodeJwt(String token) throws JWTDecodeException { return new JWTDecoder(parser, token); @@ -35,11 +40,13 @@ public DecodedJWT decodeJwt(String token) throws JWTDecodeException { /** * Decode a given Json Web Token. *

- * Note that this method doesn't verify the token's signature! Use it only if you trust the token or you already verified it. + * Note that this method doesn't verify the token's signature! + * Use it only if you trust the token or you already verified it. * * @param token with jwt format as string. * @return a decoded JWT. - * @throws JWTDecodeException if any part of the token contained an invalid jwt or JSON format of each of the jwt parts. + * @throws JWTDecodeException if any part of the token contained an invalid jwt + * or JSON format of each of the jwt parts. */ public static DecodedJWT decode(String token) throws JWTDecodeException { return new JWTDecoder(token); @@ -57,7 +64,7 @@ public static Verification require(Algorithm algorithm) { } /** - * Returns a Json Web Token builder used to create and sign tokens + * Returns a Json Web Token builder used to create and sign tokens. * * @return a token builder. */ diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index ca6e3918..6b355b36 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -15,7 +15,8 @@ import java.util.Map.Entry; /** - * The JWTCreator class holds the sign method to generate a complete JWT (with Signature) from a given Header and Payload content. + * The JWTCreator class holds the sign method to generate a complete JWT (with Signature) + * from a given Header and Payload content. *

* This class is thread-safe. */ @@ -38,7 +39,8 @@ public final class JWTCreator { mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); } - private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) throws JWTCreationException { + private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) + throws JWTCreationException { this.algorithm = algorithm; try { headerJson = mapper.writeValueAsString(new HeaderClaimsHolder(headerClaims)); @@ -96,7 +98,8 @@ public Builder withHeader(Map headerClaims) { /** * Add a specific Key Id ("kid") claim to the Header. - * If the {@link Algorithm} used to sign this token was instantiated with a KeyProvider, the 'kid' value will be taken from that provider and this one will be ignored. + * If the {@link Algorithm} used to sign this token was instantiated with a KeyProvider, + * the 'kid' value will be taken from that provider and this one will be ignored. * * @param keyId the Key Id value. * @return this same Builder instance. @@ -322,48 +325,6 @@ public Builder withClaim(String name, Instant value) throws IllegalArgumentExcep return this; } - /** - * Add a custom Array Claim with the given items. - * - * @param name the Claim's name. - * @param items the Claim's value. - * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. - */ - public Builder withArrayClaim(String name, String[] items) throws IllegalArgumentException { - assertNonNull(name); - addClaim(name, items); - return this; - } - - /** - * Add a custom Array Claim with the given items. - * - * @param name the Claim's name. - * @param items the Claim's value. - * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. - */ - public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgumentException { - assertNonNull(name); - addClaim(name, items); - return this; - } - - /** - * Add a custom Array Claim with the given items. - * - * @param name the Claim's name. - * @param items the Claim's value. - * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null - */ - public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { - assertNonNull(name); - addClaim(name, items); - return this; - } - /** * Add a custom Map Claim with the given items. *

@@ -381,7 +342,8 @@ public Builder withClaim(String name, Map map) throws IllegalArgument assertNonNull(name); // validate map contents if (map != null && !validateClaim(map)) { - throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, Long, Double, String and Date"); + throw new IllegalArgumentException("Expected map containing Map, List, Boolean, Integer, " + + "Long, Double, String and Date"); } addClaim(name, map); return this; @@ -405,12 +367,55 @@ public Builder withClaim(String name, List list) throws IllegalArgumentExcept assertNonNull(name); // validate list contents if (list != null && !validateClaim(list)) { - throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, Long, Double, String and Date"); + throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, " + + "Long, Double, String and Date"); } addClaim(name, list); return this; } + /** + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withArrayClaim(String name, String[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + + /** + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + + /** + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null + */ + public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + /** * Add specific Claims to set as the Payload. If the provided map is null then * nothing is changed. @@ -426,8 +431,9 @@ public Builder withClaim(String name, List list) throws IllegalArgumentExcept *

* * @param payloadClaims the values to use as Claims in the token's payload. - * @throws IllegalArgumentException if any of the claim keys or null, or if the values are not of a supported type. * @return this same Builder instance. + * @throws IllegalArgumentException if any of the claim keys or null, + * or if the values are not of a supported type. */ public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { if (payloadClaims == null) { @@ -435,7 +441,8 @@ public Builder withPayload(Map payloadClaims) throws IllegalArgumentE } if (!validatePayload(payloadClaims)) { - throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, " + + "Long, Double, String and Date"); } // add claims only after validating all claims so as not to corrupt the claims map of this builder @@ -504,16 +511,18 @@ private static boolean isBasicType(Object value) { if (c.isArray()) { return c == Integer[].class || c == Long[].class || c == String[].class; } - return c == String.class || c == Integer.class || c == Long.class || c == Double.class || c == Date.class || c == Instant.class || c == Boolean.class; + return c == String.class || c == Integer.class || c == Long.class || c == Double.class + || c == Date.class || c == Instant.class || c == Boolean.class; } /** - * Creates a new JWT and signs is with the given algorithm + * Creates a new JWT and signs is with the given algorithm. * * @param algorithm used to sign the JWT * @return a new JWT token * @throws IllegalArgumentException if the provided algorithm is null. - * @throws JWTCreationException if the claims could not be converted to a valid JSON or there was a problem with the signing key. + * @throws JWTCreationException if the claims could not be converted to a valid JSON + * or there was a problem with the signing key. */ public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException { if (algorithm == null) { @@ -546,10 +555,13 @@ private void addClaim(String name, Object value) { } private String sign() throws SignatureGenerationException { - String header = Base64.getUrlEncoder().withoutPadding().encodeToString(headerJson.getBytes(StandardCharsets.UTF_8)); - String payload = Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8)); + String header = Base64.getUrlEncoder().withoutPadding() + .encodeToString(headerJson.getBytes(StandardCharsets.UTF_8)); + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8)); - byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); + byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), + payload.getBytes(StandardCharsets.UTF_8)); String signature = Base64.getUrlEncoder().withoutPadding().encodeToString((signatureBytes)); return String.format("%s.%s.%s", header, payload, signature); diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index 55855241..cc283095 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -42,7 +42,7 @@ final class JWTDecoder implements DecodedJWT, Serializable { payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); } catch (NullPointerException e) { throw new JWTDecodeException("The UTF-8 Charset isn't initialized.", e); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { throw new JWTDecodeException("The input is not a valid base 64 encoded string.", e); } header = converter.parseHeader(headerJson); diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 5bcc7bc7..77dfef4e 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -16,11 +16,13 @@ import java.util.*; /** - * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also its signature matches. + * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, + * but also its signature matches. *

* This class is thread-safe. + * + * @see com.auth0.jwt.interfaces.JWTVerifier */ -@SuppressWarnings("WeakerAccess") public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { private final Algorithm algorithm; final Map claims; @@ -48,6 +50,9 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { return new BaseVerification(algorithm); } + /** + * {@link Verification} implementation that accepts all the expected Claim values for verification. + */ public static class BaseVerification implements Verification { private final Algorithm algorithm; private final Map claims; @@ -180,7 +185,8 @@ public Verification withClaim(String name, Date value) throws IllegalArgumentExc @Override public Verification withClaim(String name, Instant value) throws IllegalArgumentException { assertNonNull(name); - // Since date-time claims are serialized as epoch seconds, we need to compare them with only seconds-granularity + // Since date-time claims are serialized as epoch seconds, + // we need to compare them with only seconds-granularity requireClaim(name, value != null ? value.truncatedTo(ChronoUnit.SECONDS) : null); return this; } @@ -280,7 +286,8 @@ private static boolean isNullOrEmpty(String[] args) { * * @param token to verify. * @return a verified and decoded JWT. - * @throws AlgorithmMismatchException if the algorithm stated in the token's header is not equal to the one defined in the {@link JWTVerifier}. + * @throws AlgorithmMismatchException if the algorithm stated in the token's header is not equal to + * the one defined in the {@link JWTVerifier}. * @throws SignatureVerificationException if the signature is invalid. * @throws TokenExpiredException if the token has expired. * @throws InvalidClaimException if a claim contained a different value than the expected one. @@ -296,7 +303,8 @@ public DecodedJWT verify(String token) throws JWTVerificationException { * * @param jwt to verify. * @return a verified and decoded JWT. - * @throws AlgorithmMismatchException if the algorithm stated in the token's header is not equal to the one defined in the {@link JWTVerifier}. + * @throws AlgorithmMismatchException if the algorithm stated in the token's header is not equal to + * the one defined in the {@link JWTVerifier}. * @throws SignatureVerificationException if the signature is invalid. * @throws TokenExpiredException if the token has expired. * @throws InvalidClaimException if a claim contained a different value than the expected one. @@ -311,11 +319,13 @@ public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException { private void verifyAlgorithm(DecodedJWT jwt, Algorithm expectedAlgorithm) throws AlgorithmMismatchException { if (!expectedAlgorithm.getName().equals(jwt.getAlgorithm())) { - throw new AlgorithmMismatchException("The provided Algorithm doesn't match the one defined in the JWT's Header."); + throw new AlgorithmMismatchException( + "The provided Algorithm doesn't match the one defined in the JWT's Header."); } } - private void verifyClaims(DecodedJWT jwt, Map claims) throws TokenExpiredException, InvalidClaimException { + private void verifyClaims(DecodedJWT jwt, Map claims) + throws TokenExpiredException, InvalidClaimException { for (Map.Entry entry : claims.entrySet()) { if (entry.getValue() instanceof NonEmptyClaim) { assertClaimPresent(jwt.getClaim(entry.getKey()), entry.getKey()); @@ -327,8 +337,9 @@ private void verifyClaims(DecodedJWT jwt, Map claims) throws Tok private void verifyClaimValues(DecodedJWT jwt, Map.Entry expectedClaim) { switch (expectedClaim.getKey()) { - // We use custom keys for audience in the expected claims to differentiate between validating that the audience - // contains all expected values, or validating that the audience contains at least one of the expected values. + // We use custom keys for audience in the expected claims to differentiate between + // validating that the audience contains all expected values, or validating that the audience contains + // at least one of the expected values. case AUDIENCE_EXACT: assertValidAudienceClaim(jwt.getAudience(), (List) expectedClaim.getValue(), true); break; @@ -354,7 +365,8 @@ private void verifyClaimValues(DecodedJWT jwt, Map.Entry expecte assertValidStringClaim(expectedClaim.getKey(), jwt.getSubject(), (String) expectedClaim.getValue()); break; default: - assertValidClaim(jwt.getClaim(expectedClaim.getKey()), expectedClaim.getKey(), expectedClaim.getValue()); + assertValidClaim(jwt.getClaim(expectedClaim.getKey()), expectedClaim.getKey(), + expectedClaim.getValue()); break; } } @@ -402,13 +414,15 @@ private void assertValidClaim(Claim claim, String claimName, Object value) { } if (!isValid) { - throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName)); + throw new InvalidClaimException( + String.format("The Claim '%s' value doesn't match the required one.", claimName)); } } private void assertValidStringClaim(String claimName, String value, String expectedValue) { if (!expectedValue.equals(value)) { - throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName)); + throw new InvalidClaimException( + String.format("The Claim '%s' value doesn't match the required one.", claimName)); } } @@ -434,8 +448,8 @@ private void assertInstantIsPast(Instant claimVal, long leeway, Instant now) { } private void assertValidAudienceClaim(List audience, List values, boolean shouldContainAll) { - if (audience == null || (shouldContainAll && !audience.containsAll(values)) || - (!shouldContainAll && Collections.disjoint(audience, values))) { + if (audience == null || (shouldContainAll && !audience.containsAll(values)) + || (!shouldContainAll && Collections.disjoint(audience, values))) { throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); } } @@ -452,7 +466,8 @@ private void assertValidIssuerClaim(String issuer, List value) { private static class NonEmptyClaim { private static NonEmptyClaim nonEmptyClaim; - private NonEmptyClaim() {} + private NonEmptyClaim() { + } public static NonEmptyClaim getInstance() { if (nonEmptyClaim == null) { diff --git a/lib/src/main/java/com/auth0/jwt/TokenUtils.java b/lib/src/main/java/com/auth0/jwt/TokenUtils.java index cb6cff3e..0a76028b 100644 --- a/lib/src/main/java/com/auth0/jwt/TokenUtils.java +++ b/lib/src/main/java/com/auth0/jwt/TokenUtils.java @@ -18,7 +18,8 @@ static String[] splitToken(String token) throws JWTDecodeException { parts = new String[]{parts[0], parts[1], ""}; } if (parts.length != 3) { - throw new JWTDecodeException(String.format("The token was expected to have 3 parts, but got %s.", parts.length)); + throw new JWTDecodeException( + String.format("The token was expected to have 3 parts, but got %s.", parts.length)); } return parts; } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index ff10a9aa..a5247005 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -139,47 +139,47 @@ public static Algorithm HMAC256(String secret) throws IllegalArgumentException { } /** - * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". + * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". * - * @param secret the secret to use in the verify or signing instance. - * @return a valid HMAC384 Algorithm. + * @param secret the secret bytes to use in the verify or signing instance. + * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ - public static Algorithm HMAC384(String secret) throws IllegalArgumentException { - return new HMACAlgorithm("HS384", "HmacSHA384", secret); + public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS256", "HmacSHA256", secret); } /** - * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". + * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * * @param secret the secret to use in the verify or signing instance. - * @return a valid HMAC512 Algorithm. + * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ - public static Algorithm HMAC512(String secret) throws IllegalArgumentException { - return new HMACAlgorithm("HS512", "HmacSHA512", secret); + public static Algorithm HMAC384(String secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS384", "HmacSHA384", secret); } /** - * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". + * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * * @param secret the secret bytes to use in the verify or signing instance. - * @return a valid HMAC256 Algorithm. + * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ - public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { - return new HMACAlgorithm("HS256", "HmacSHA256", secret); + public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS384", "HmacSHA384", secret); } /** - * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". + * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". * - * @param secret the secret bytes to use in the verify or signing instance. - * @return a valid HMAC384 Algorithm. + * @param secret the secret to use in the verify or signing instance. + * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ - public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { - return new HMACAlgorithm("HS384", "HmacSHA384", secret); + public static Algorithm HMAC512(String secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS512", "HmacSHA512", secret); } /** @@ -192,8 +192,7 @@ public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { public static Algorithm HMAC512(byte[] secret) throws IllegalArgumentException { return new HMACAlgorithm("HS512", "HmacSHA512", secret); } - - + /** * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". @@ -314,7 +313,8 @@ protected Algorithm(String name, String description) { } /** - * Getter for the Id of the Private Key used to sign the tokens. This is usually specified as the `kid` claim in the Header. + * Getter for the Id of the Private Key used to sign the tokens. + * This is usually specified as the `kid` claim in the Header. * * @return the Key Id that identifies the Signing Key or null if it's not specified. */ @@ -332,7 +332,8 @@ public String getName() { } /** - * Getter for the description of this Algorithm, required when instantiating a Mac or Signature object. i.e. "HmacSHA256" + * Getter for the description of this Algorithm, + * required when instantiating a Mac or Signature object. i.e. "HmacSHA256" * * @return the algorithm description. */ @@ -349,15 +350,19 @@ public String toString() { * Verify the given token using this Algorithm instance. * * @param jwt the already decoded JWT that it's going to be verified. - * @throws SignatureVerificationException if the Token's Signature is invalid, meaning that it doesn't match the signatureBytes, or if the Key is invalid. + * @throws SignatureVerificationException if the Token's Signature is invalid, + * meaning that it doesn't match the signatureBytes, + * or if the Key is invalid. */ public abstract void verify(DecodedJWT jwt) throws SignatureVerificationException; /** * Sign the given content using this Algorithm instance. * - * @param headerBytes an array of bytes representing the base64 encoded header content to be verified against the signature. - * @param payloadBytes an array of bytes representing the base64 encoded payload content to be verified against the signature. + * @param headerBytes an array of bytes representing the base64 encoded header content + * to be verified against the signature. + * @param payloadBytes an array of bytes representing the base64 encoded payload content + * to be verified against the signature. * @return the signature in a base64 encoded array of bytes * @throws SignatureGenerationException if the Key is invalid. */ @@ -376,7 +381,8 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene * Sign the given content using this Algorithm instance. * To get the correct JWT Signature, ensure the content is in the format {HEADER}.{PAYLOAD} * - * @param contentBytes an array of bytes representing the base64 encoded content to be verified against the signature. + * @param contentBytes an array of bytes representing the base64 encoded content + * to be verified against the signature. * @return the signature in a base64 encoded array of bytes * @throws SignatureGenerationException if the Key is invalid. */ diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index 26a87bc4..7b8c5c2a 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -12,93 +12,98 @@ */ class CryptoHelper { - private static final byte JWT_PART_SEPARATOR = (byte)46; + private static final byte JWT_PART_SEPARATOR = (byte) 46; /** * Verify signature for JWT header and payload. * - * @param algorithm algorithm name. - * @param secretBytes algorithm secret. - * @param header JWT header. - * @param payload JWT payload. + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param header JWT header. + * @param payload JWT payload. * @param signatureBytes JWT signature. * @return true if signature is valid. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - boolean verifySignatureFor(String algorithm, byte[] secretBytes, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { - return verifySignatureFor(algorithm, secretBytes, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + boolean verifySignatureFor( + String algorithm, + byte[] secretBytes, + String header, + String payload, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException { + return verifySignatureFor(algorithm, secretBytes, + header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); } /** * Verify signature for JWT header and payload. * - * @param algorithm algorithm name. - * @param secretBytes algorithm secret. - * @param headerBytes JWT header. - * @param payloadBytes JWT payload. + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. * @param signatureBytes JWT signature. * @return true if signature is valid. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { - return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), signatureBytes); - } - - /** - * Create signature for JWT header and payload. - * - * @param algorithm algorithm name. - * @param secretBytes algorithm secret. - * @param headerBytes JWT header. - * @param payloadBytes JWT payload. - * @return the signature bytes. - * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. - */ - - byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException { - final Mac mac = Mac.getInstance(algorithm); - mac.init(new SecretKeySpec(secretBytes, algorithm)); - mac.update(headerBytes); - mac.update(JWT_PART_SEPARATOR); - return mac.doFinal(payloadBytes); + boolean verifySignatureFor( + String algorithm, + byte[] secretBytes, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException { + return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), + signatureBytes); } /** * Verify signature for JWT header and payload. * - * @param algorithm algorithm name. - * @param publicKey algorithm public key. - * @param header JWT header. - * @param payload JWT payload. + * @param algorithm algorithm name. + * @param publicKey algorithm public key. + * @param header JWT header. + * @param payload JWT payload. * @param signatureBytes JWT signature. * @return true if signature is valid. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - boolean verifySignatureFor(String algorithm, PublicKey publicKey, String header, String payload, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - return verifySignatureFor(algorithm, publicKey, header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + String header, + String payload, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return verifySignatureFor(algorithm, publicKey, header.getBytes(StandardCharsets.UTF_8), + payload.getBytes(StandardCharsets.UTF_8), signatureBytes); } /** * Verify signature for JWT header and payload using a public key. * - * @param algorithm algorithm name. - * @param publicKey the public key to use for verification. - * @param headerBytes JWT header. - * @param payloadBytes JWT payload. + * @param algorithm algorithm name. + * @param publicKey the public key to use for verification. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. * @param signatureBytes JWT signature. * @return true if signature is valid. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] headerBytes, byte[] payloadBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initVerify(publicKey); s.update(headerBytes); @@ -110,17 +115,22 @@ boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] headerB /** * Create signature for JWT header and payload using a private key. * - * @param algorithm algorithm name. - * @param privateKey the private key to use for signing. - * @param headerBytes JWT header. + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param headerBytes JWT header. * @param payloadBytes JWT payload. * @return the signature bytes. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. - * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly + * or if this signature algorithm is unable to process the input data provided. */ - - byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] headerBytes, byte[] payloadBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + byte[] createSignatureFor( + String algorithm, + PrivateKey privateKey, + byte[] headerBytes, + byte[] payloadBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initSign(privateKey); s.update(headerBytes); @@ -130,75 +140,66 @@ byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] header } /** - * Verify signature. - * For valid verification, ensure the content is in the format {HEADER}.{PAYLOAD} + * Create signature for JWT header and payload. * - * @param algorithm algorithm name. - * @param secretBytes algorithm secret. - * @param contentBytes the content to which the signature applies. - * @param signatureBytes JWT signature. - * @return true if signature is valid. + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @return the signature bytes. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { - return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, contentBytes), signatureBytes); + byte[] createSignatureFor( + String algorithm, + byte[] secretBytes, + byte[] headerBytes, + byte[] payloadBytes + ) throws NoSuchAlgorithmException, InvalidKeyException { + final Mac mac = Mac.getInstance(algorithm); + mac.init(new SecretKeySpec(secretBytes, algorithm)); + mac.update(headerBytes); + mac.update(JWT_PART_SEPARATOR); + return mac.doFinal(payloadBytes); } /** * Create signature. * To get the correct JWT Signature, ensure the content is in the format {HEADER}.{PAYLOAD} * - * @param algorithm algorithm name. - * @param secretBytes algorithm secret. + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. * @param contentBytes the content to be signed. * @return the signature bytes. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes) throws NoSuchAlgorithmException, InvalidKeyException { + byte[] createSignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes) + throws NoSuchAlgorithmException, InvalidKeyException { final Mac mac = Mac.getInstance(algorithm); mac.init(new SecretKeySpec(secretBytes, algorithm)); return mac.doFinal(contentBytes); } - /** - * Verify signature using a public key. - * For valid verification, ensure the content is in the format {HEADER}.{PAYLOAD} - * - * @param algorithm algorithm name. - * @param publicKey algorithm public key. - * @param contentBytes the content to which the signature applies. - * @param signatureBytes JWT signature. - * @return the signature bytes. - * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. - * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. - */ - - boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - final Signature s = Signature.getInstance(algorithm); - s.initVerify(publicKey); - s.update(contentBytes); - return s.verify(signatureBytes); - } - /** * Create signature using a private key. * To get the correct JWT Signature, ensure the content is in the format {HEADER}.{PAYLOAD} * - * @param algorithm algorithm name. - * @param privateKey the private key to use for signing. + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. * @param contentBytes the content to be signed. * @return the signature bytes. * @throws NoSuchAlgorithmException if the algorithm is not supported. - * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. - * @throws SignatureException if this signature object is not initialized properly or if this signature algorithm is unable to process the input data provided. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly + * or if this signature algorithm is unable to process the input data provided. */ - byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] contentBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + byte[] createSignatureFor( + String algorithm, + PrivateKey privateKey, + byte[] contentBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initSign(privateKey); s.update(contentBytes); diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 51fa70fa..26700a2a 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -24,7 +24,8 @@ class ECDSAAlgorithm extends Algorithm { private final int ecNumberSize; //Visible for testing - ECDSAAlgorithm(CryptoHelper crypto, String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + ECDSAAlgorithm(CryptoHelper crypto, String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) + throws IllegalArgumentException { super(id, algorithm); if (keyProvider == null) { throw new IllegalArgumentException("The Key Provider cannot be null."); @@ -34,7 +35,8 @@ class ECDSAAlgorithm extends Algorithm { this.ecNumberSize = ecNumberSize; } - ECDSAAlgorithm(String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + ECDSAAlgorithm(String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) + throws IllegalArgumentException { this(new CryptoHelper(), id, algorithm, ecNumberSize, keyProvider); } @@ -46,12 +48,14 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); } - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); + boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), + jwt.getPayload(), JOSEToDER(signatureBytes)); if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException | IllegalArgumentException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException + | IllegalStateException | IllegalArgumentException e) { throw new SignatureVerificationException(this, e); } } @@ -116,25 +120,27 @@ byte[] DERToJOSE(byte[] derSignature) throws SignatureException { offset++; //Obtain R number length (Includes padding) and skip it - int rLength = derSignature[offset++]; - if (rLength > ecNumberSize + 1) { + int rlength = derSignature[offset++]; + if (rlength > ecNumberSize + 1) { throw new SignatureException("Invalid DER signature format."); } - int rPadding = ecNumberSize - rLength; + int rpadding = ecNumberSize - rlength; //Retrieve R number - System.arraycopy(derSignature, offset + Math.max(-rPadding, 0), joseSignature, Math.max(rPadding, 0), rLength + Math.min(rPadding, 0)); + System.arraycopy(derSignature, offset + Math.max(-rpadding, 0), + joseSignature, Math.max(rpadding, 0), rlength + Math.min(rpadding, 0)); //Skip R number and 0x02 - offset += rLength + 1; + offset += rlength + 1; //Obtain S number length. (Includes padding) - int sLength = derSignature[offset++]; - if (sLength > ecNumberSize + 1) { + int slength = derSignature[offset++]; + if (slength > ecNumberSize + 1) { throw new SignatureException("Invalid DER signature format."); } - int sPadding = ecNumberSize - sLength; + int spadding = ecNumberSize - slength; //Retrieve R number - System.arraycopy(derSignature, offset + Math.max(-sPadding, 0), joseSignature, ecNumberSize + Math.max(sPadding, 0), sLength + Math.min(sPadding, 0)); + System.arraycopy(derSignature, offset + Math.max(-spadding, 0), joseSignature, + ecNumberSize + Math.max(spadding, 0), slength + Math.min(spadding, 0)); return joseSignature; } @@ -146,12 +152,12 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { } // Retrieve R and S number's length and padding. - int rPadding = countPadding(joseSignature, 0, ecNumberSize); - int sPadding = countPadding(joseSignature, ecNumberSize, joseSignature.length); - int rLength = ecNumberSize - rPadding; - int sLength = ecNumberSize - sPadding; + int rpadding = countPadding(joseSignature, 0, ecNumberSize); + int spadding = countPadding(joseSignature, ecNumberSize, joseSignature.length); + int rlength = ecNumberSize - rpadding; + int slength = ecNumberSize - spadding; - int length = 2 + rLength + 2 + sLength; + int length = 2 + rlength + 2 + slength; if (length > 255) { throw new SignatureException("Invalid JOSE signature format."); } @@ -174,31 +180,32 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { // Header with "min R" number length derSignature[offset++] = (byte) 0x02; - derSignature[offset++] = (byte) rLength; + derSignature[offset++] = (byte) rlength; // R number - if (rPadding < 0) { + if (rpadding < 0) { //Sign derSignature[offset++] = (byte) 0x00; System.arraycopy(joseSignature, 0, derSignature, offset, ecNumberSize); offset += ecNumberSize; } else { - int copyLength = Math.min(ecNumberSize, rLength); - System.arraycopy(joseSignature, rPadding, derSignature, offset, copyLength); + int copyLength = Math.min(ecNumberSize, rlength); + System.arraycopy(joseSignature, rpadding, derSignature, offset, copyLength); offset += copyLength; } // Header with "min S" number length derSignature[offset++] = (byte) 0x02; - derSignature[offset++] = (byte) sLength; + derSignature[offset++] = (byte) slength; // S number - if (sPadding < 0) { + if (spadding < 0) { //Sign derSignature[offset++] = (byte) 0x00; System.arraycopy(joseSignature, ecNumberSize, derSignature, offset, ecNumberSize); } else { - System.arraycopy(joseSignature, ecNumberSize + sPadding, derSignature, offset, Math.min(ecNumberSize, sLength)); + System.arraycopy(joseSignature, ecNumberSize + spadding, derSignature, offset, + Math.min(ecNumberSize, slength)); } return derSignature; diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 26ad6a87..0306e7c4 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -21,7 +21,8 @@ class HMACAlgorithm extends Algorithm { private final byte[] secret; //Visible for testing - HMACAlgorithm(CryptoHelper crypto, String id, String algorithm, byte[] secretBytes) throws IllegalArgumentException { + HMACAlgorithm(CryptoHelper crypto, String id, String algorithm, byte[] secretBytes) + throws IllegalArgumentException { super(id, algorithm); if (secretBytes == null) { throw new IllegalArgumentException("The Secret cannot be null"); @@ -50,7 +51,8 @@ static byte[] getSecretBytes(String secret) throws IllegalArgumentException { public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { byte[] signatureBytes = Base64.getUrlDecoder().decode(jwt.getSignature()); - boolean valid = crypto.verifySignatureFor(getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); + boolean valid = crypto.verifySignatureFor( + getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java index ade07b0e..5c6c0fc5 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java @@ -3,7 +3,6 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; - import java.util.Base64; class NoneAlgorithm extends Algorithm { diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index cd1e97a5..0c7a5b57 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -24,7 +24,8 @@ class RSAAlgorithm extends Algorithm { private final CryptoHelper crypto; //Visible for testing - RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKeyProvider keyProvider) throws IllegalArgumentException { + RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKeyProvider keyProvider) + throws IllegalArgumentException { super(id, algorithm); if (keyProvider == null) { throw new IllegalArgumentException("The Key Provider cannot be null."); @@ -45,11 +46,13 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); } - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes); + boolean valid = crypto.verifySignatureFor( + getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException | IllegalStateException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException + | IllegalArgumentException | IllegalStateException e) { throw new SignatureVerificationException(this, e); } } @@ -66,7 +69,7 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene throw new SignatureGenerationException(this, e); } } - + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/AlgorithmMismatchException.java b/lib/src/main/java/com/auth0/jwt/exceptions/AlgorithmMismatchException.java index 1d7ba1ce..d6b71205 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/AlgorithmMismatchException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/AlgorithmMismatchException.java @@ -1,5 +1,8 @@ package com.auth0.jwt.exceptions; +/** + * The exception that will be thrown if the exception doesn't match the one mentioned in the JWT Header. + */ public class AlgorithmMismatchException extends JWTVerificationException { public AlgorithmMismatchException(String message) { super(message); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java b/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java index ab348323..f80fb752 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java @@ -1,6 +1,8 @@ package com.auth0.jwt.exceptions; - +/** + * The exception that will be thrown while verifying Claims of a JWT. + */ public class InvalidClaimException extends JWTVerificationException { public InvalidClaimException(String message) { super(message); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/JWTCreationException.java b/lib/src/main/java/com/auth0/jwt/exceptions/JWTCreationException.java index 5bf4facb..c7e162ea 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/JWTCreationException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/JWTCreationException.java @@ -1,5 +1,8 @@ package com.auth0.jwt.exceptions; +/** + * The exception that is thrown when a JWT cannot be created. + */ public class JWTCreationException extends RuntimeException { public JWTCreationException(String message, Throwable cause) { super(message, cause); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/JWTDecodeException.java b/lib/src/main/java/com/auth0/jwt/exceptions/JWTDecodeException.java index 93799d31..448714e9 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/JWTDecodeException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/JWTDecodeException.java @@ -1,5 +1,8 @@ package com.auth0.jwt.exceptions; +/** + * The exception that is thrown when any part of the token contained an invalid JWT or JSON format. + */ public class JWTDecodeException extends JWTVerificationException { public JWTDecodeException(String message) { this(message, null); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/JWTVerificationException.java b/lib/src/main/java/com/auth0/jwt/exceptions/JWTVerificationException.java index 2ccfcccf..dd36dcd3 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/JWTVerificationException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/JWTVerificationException.java @@ -1,5 +1,8 @@ package com.auth0.jwt.exceptions; +/** + * Parent to all the exception thrown while verifying a JWT. + */ public class JWTVerificationException extends RuntimeException { public JWTVerificationException(String message) { this(message, null); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/SignatureGenerationException.java b/lib/src/main/java/com/auth0/jwt/exceptions/SignatureGenerationException.java index 3637f97a..4b7668a0 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/SignatureGenerationException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/SignatureGenerationException.java @@ -2,6 +2,9 @@ import com.auth0.jwt.algorithms.Algorithm; +/** + * The exception that is thrown when signature is not able to be generated. + */ public class SignatureGenerationException extends JWTCreationException { public SignatureGenerationException(Algorithm algorithm, Throwable cause) { super("The Token's Signature couldn't be generated when signing using the Algorithm: " + algorithm, cause); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/SignatureVerificationException.java b/lib/src/main/java/com/auth0/jwt/exceptions/SignatureVerificationException.java index 12bd3429..fa7c3cab 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/SignatureVerificationException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/SignatureVerificationException.java @@ -2,6 +2,9 @@ import com.auth0.jwt.algorithms.Algorithm; +/** + * The exception that is thrown if the Signature verification fails. + */ public class SignatureVerificationException extends JWTVerificationException { public SignatureVerificationException(Algorithm algorithm) { this(algorithm, null); diff --git a/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java b/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java index f5d2e67a..66f28285 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java @@ -1,5 +1,8 @@ package com.auth0.jwt.exceptions; +/** + * The exception that is thrown if the token is expired. + */ public class TokenExpiredException extends JWTVerificationException { private static final long serialVersionUID = -7076928975713577708L; diff --git a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java index 0a782268..3746dcd2 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java +++ b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java @@ -24,8 +24,15 @@ class BasicHeader implements Header, Serializable { private final String keyId; private final Map tree; private final ObjectReader objectReader; - - BasicHeader(String algorithm, String type, String contentType, String keyId, Map tree, ObjectReader objectReader) { + + BasicHeader( + String algorithm, + String type, + String contentType, + String keyId, + Map tree, + ObjectReader objectReader + ) { this.algorithm = algorithm; this.type = type; this.contentType = contentType; diff --git a/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java index 4db20119..b1f8e6d3 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java @@ -32,12 +32,12 @@ public void serialize(T holder, JsonGenerator gen, SerializerProvider provider) /** * Writes the given entry to the JSON representation. Custom claim serialization handling can override this method - * to provide use-case specific serialization. Implementors who override this method must write the field name and the - * field value. + * to provide use-case specific serialization. Implementors who override this method must write + * the field name and the field value. * * @param entry The entry that corresponds to the JSON field to write * @param gen The {@code JsonGenerator} to use - * @throws IOException + * @throws IOException if there is either an underlying I/O problem or encoding issue at format layer */ protected void writeClaim(Map.Entry entry, JsonGenerator gen) throws IOException { gen.writeFieldName(entry.getKey()); diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java index 6bba2caa..8a6cbcf8 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -13,10 +13,10 @@ /** * Jackson deserializer implementation for converting from JWT Header parts. - * - * @see JWTParser *

* This class is thread-safe. + * + * @see JWTParser */ class HeaderDeserializer extends StdDeserializer { diff --git a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java index 84a8b867..fe1600bd 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java @@ -9,9 +9,12 @@ import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; - import java.io.IOException; +/** + * This class helps in decoding the Header and Payload of the JWT using + * {@link HeaderSerializer} and {@link PayloadSerializer}. + */ public class JWTParser implements JWTPartsParser { private final ObjectReader payloadReader; private final ObjectReader headerReader; diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 0d816128..54a09575 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -74,24 +74,24 @@ public Instant asInstant() { @Override @SuppressWarnings("unchecked") - public T[] asArray(Class tClazz) throws JWTDecodeException { + public T[] asArray(Class clazz) throws JWTDecodeException { if (!data.isArray()) { return null; } - T[] arr = (T[]) Array.newInstance(tClazz, data.size()); + T[] arr = (T[]) Array.newInstance(clazz, data.size()); for (int i = 0; i < data.size(); i++) { try { - arr[i] = objectReader.treeToValue(data.get(i), tClazz); + arr[i] = objectReader.treeToValue(data.get(i), clazz); } catch (JsonProcessingException e) { - throw new JWTDecodeException("Couldn't map the Claim's array contents to " + tClazz.getSimpleName(), e); + throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e); } } return arr; } @Override - public List asList(Class tClazz) throws JWTDecodeException { + public List asList(Class clazz) throws JWTDecodeException { if (!data.isArray()) { return null; } @@ -99,9 +99,9 @@ public List asList(Class tClazz) throws JWTDecodeException { List list = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { try { - list.add(objectReader.treeToValue(data.get(i), tClazz)); + list.add(objectReader.treeToValue(data.get(i), clazz)); } catch (JsonProcessingException e) { - throw new JWTDecodeException("Couldn't map the Claim's array contents to " + tClazz.getSimpleName(), e); + throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e); } } return list; @@ -124,11 +124,11 @@ public Map asMap() throws JWTDecodeException { } @Override - public T as(Class tClazz) throws JWTDecodeException { + public T as(Class clazz) throws JWTDecodeException { try { - return objectReader.treeAsTokens(data).readValueAs(tClazz); + return objectReader.treeAsTokens(data).readValueAs(clazz); } catch (IOException e) { - throw new JWTDecodeException("Couldn't map the Claim value to " + tClazz.getSimpleName(), e); + throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e); } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java index 664328de..2975b51b 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java @@ -53,12 +53,12 @@ public Instant asInstant() { } @Override - public T[] asArray(Class tClazz) throws JWTDecodeException { + public T[] asArray(Class clazz) throws JWTDecodeException { return null; } @Override - public List asList(Class tClazz) throws JWTDecodeException { + public List asList(Class clazz) throws JWTDecodeException { return null; } @@ -68,7 +68,7 @@ public Map asMap() throws JWTDecodeException { } @Override - public T as(Class tClazz) throws JWTDecodeException { + public T as(Class clazz) throws JWTDecodeException { return null; } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 46bd7ebe..09009267 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -16,10 +16,10 @@ /** * Jackson deserializer implementation for converting from JWT Payload parts. - * - * @see JWTParser *

* This class is thread-safe. + * + * @see JWTParser */ class PayloadDeserializer extends StdDeserializer { @@ -80,7 +80,8 @@ Instant getInstantFromSeconds(Map tree, String claimName) { return null; } if (!node.canConvertToLong()) { - throw new JWTDecodeException(String.format("The claim '%s' contained a non-numeric date value.", claimName)); + throw new JWTDecodeException( + String.format("The claim '%s' contained a non-numeric date value.", claimName)); } return Instant.ofEpochSecond(node.asLong()); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index a446741c..75e79474 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -13,10 +13,10 @@ /** * Decoder of string JSON Web Tokens into their POJO representations. - * - * @see Payload *

* This class is thread-safe. + * + * @see Payload */ class PayloadImpl implements Payload, Serializable { @@ -32,7 +32,17 @@ class PayloadImpl implements Payload, Serializable { private final Map tree; private final ObjectReader objectReader; - PayloadImpl(String issuer, String subject, List audience, Instant expiresAt, Instant notBefore, Instant issuedAt, String jwtId, Map tree, ObjectReader objectReader) { + PayloadImpl( + String issuer, + String subject, + List audience, + Instant expiresAt, + Instant notBefore, + Instant issuedAt, + String jwtId, + Map tree, + ObjectReader objectReader + ) { this.issuer = issuer; this.subject = subject; this.audience = audience != null ? Collections.unmodifiableList(audience) : null; diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index 497a3a94..dc138c08 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -10,10 +10,10 @@ /** * Jackson serializer implementation for converting into JWT Payload parts. - * - * @see com.auth0.jwt.JWTCreator *

* This class is thread-safe. + * + * @see com.auth0.jwt.JWTCreator */ public class PayloadSerializer extends ClaimsSerializer { public PayloadSerializer() { @@ -47,7 +47,7 @@ private void writeAudience(JsonGenerator gen, Map.Entry e) throw List audList = (List) e.getValue(); for (Object aud : audList) { if (aud instanceof String) { - audArray.add((String)aud); + audArray.add((String) aud); } } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java b/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java index cc2b3db8..ea166b0d 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java @@ -1,6 +1,8 @@ package com.auth0.jwt.impl; - +/** + * Contains the claim name for all Public claims. + */ public interface PublicClaims { //Header diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index cc5256f4..fdfac715 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -81,23 +81,24 @@ default Instant asInstant() { /** * Get this Claim as an Array of type T. * If the value isn't an Array, null will be returned. + * * @param type - * @param tClazz the type class + * @param clazz the type class * @return the value as an Array or null. * @throws JWTDecodeException if the values inside the Array can't be converted to a class T. */ - T[] asArray(Class tClazz) throws JWTDecodeException; + T[] asArray(Class clazz) throws JWTDecodeException; /** * Get this Claim as a List of type T. * If the value isn't an Array, null will be returned. * * @param type - * @param tClazz the type class + * @param clazz the type class * @return the value as a List or null. * @throws JWTDecodeException if the values inside the List can't be converted to a class T. */ - List asList(Class tClazz) throws JWTDecodeException; + List asList(Class clazz) throws JWTDecodeException; /** * Get this Claim as a generic Map of values. @@ -111,9 +112,9 @@ default Instant asInstant() { * Get this Claim as a custom type T. * * @param type - * @param tClazz the type class + * @param clazz the type class * @return the value as instance of T. * @throws JWTDecodeException if the value can't be converted to a class T. */ - T as(Class tClazz) throws JWTDecodeException; + T as(Class clazz) throws JWTDecodeException; } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java index 520e35c8..e1c6efcc 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java @@ -3,7 +3,8 @@ import com.auth0.jwt.exceptions.JWTDecodeException; /** - * The JWTPartsParser class defines which parts of the JWT should be converted to it's specific Object representation instance. + * The JWTPartsParser class defines which parts of the JWT should be converted + * to it's specific Object representation instance. */ public interface JWTPartsParser { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index a335a83e..3555878e 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -3,10 +3,13 @@ import com.auth0.jwt.exceptions.JWTVerificationException; +/** + * Used to verify the JWT for it's signature and claims. + */ public interface JWTVerifier { /** - * Performs the verification against the given Token + * Performs the verification against the given Token. * * @param token to verify. * @return a verified and decoded JWT. @@ -15,7 +18,7 @@ public interface JWTVerifier { DecodedJWT verify(String token) throws JWTVerificationException; /** - * Performs the verification against the given decoded JWT + * Performs the verification against the given decoded JWT. * * @param jwt to verify. * @return a verified and decoded JWT. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java index fa3b13c9..fd8baea5 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java @@ -14,7 +14,8 @@ interface KeyProvider { /** * Getter for the Public Key instance with the given Id. Used to verify the signature on the JWT verification stage. * - * @param keyId the Key Id specified in the Token's Header or null if none is available. Provides a hint on which Public Key to use to verify the token's signature. + * @param keyId the Key Id specified in the Token's Header or null if none is available. + * Provides a hint on which Public Key to use to verify the token's signature. * @return the Public Key instance */ U getPublicKeyById(String keyId); @@ -27,7 +28,8 @@ interface KeyProvider { R getPrivateKey(); /** - * Getter for the Id of the Private Key used to sign the tokens. This represents the `kid` claim and will be placed in the Header. + * Getter for the Id of the Private Key used to sign the tokens. + * This represents the `kid` claim and will be placed in the Header. * * @return the Key Id that identifies the Private Key or null if it's not specified. */ diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 1fb6aece..071b3e9c 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -60,7 +60,7 @@ default Verification withIssuer(String issuer) { * will have to provide their own implementation. * * The default implementation throws an {@linkplain UnsupportedOperationException}. - * + * * @param audience the required Audience value for which the "aud" claim must contain at least one value. * @return this same Verification instance. */ @@ -69,8 +69,8 @@ default Verification withAnyOfAudience(String... audience) { } /** - * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - * Setting a specific leeway value on a given Claim will override this value for that Claim. + * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims + * will still be valid. Setting a specific leeway value on a given Claim will override this value for that Claim. * * @param leeway the window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. * @return this same Verification instance. @@ -80,7 +80,8 @@ default Verification withAnyOfAudience(String... audience) { /** * Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. - * Expiration Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * Expiration Date is always verified when the value is present. + * This method overrides the value set with acceptLeeway * * @param leeway the window in seconds in which the Expires At Claim will still be valid. * @return this same Verification instance. @@ -90,7 +91,8 @@ default Verification withAnyOfAudience(String... audience) { /** * Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. - * Not Before Date is always verified when the value is present. This method overrides the value set with acceptLeeway + * Not Before Date is always verified when the value is present. + * This method overrides the value set with acceptLeeway * * @param leeway the window in seconds in which the Not Before Claim will still be valid. * @return this same Verification instance. @@ -101,8 +103,10 @@ default Verification withAnyOfAudience(String... audience) { /** * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. * This method overrides the value set with {@link #acceptLeeway(long)}. - * By default, the Issued At claim is always verified when the value is present, unless disabled with {@link #ignoreIssuedAt()}. - * If Issued At verification has been disabled, no verification of the Issued At claim will be performed, and this method has no effect. + * By default, the Issued At claim is always verified when the value is present, + * unless disabled with {@link #ignoreIssuedAt()}. + * If Issued At verification has been disabled, no verification of the Issued At claim will be performed, + * and this method has no effect. * * @param leeway the window in seconds in which the Issued At Claim will still be valid. * @return this same Verification instance. @@ -120,6 +124,7 @@ default Verification withAnyOfAudience(String... audience) { /** * Require a claim to be present, with any value. + * * @param name the Claim's name. * @return this same Verification instance * @throws IllegalArgumentException if the name is null. @@ -177,8 +182,8 @@ default Verification withAnyOfAudience(String... audience) { Verification withClaim(String name, String value) throws IllegalArgumentException; /** - * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; when verifying - * date-time claim value, any time units more granular than seconds will not be considered. + * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; + * when verifying date-time claim value, any time units more granular than seconds will not be considered. * * @param name the Claim's name. * @param value the Claim's value. @@ -188,8 +193,8 @@ default Verification withAnyOfAudience(String... audience) { Verification withClaim(String name, Date value) throws IllegalArgumentException; /** - * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; when verifying - * a date-time claim value, any time units more granular than seconds will not be considered. + * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; + * when verifying a date-time claim value, any time units more granular than seconds will not be considered. * * @param name the Claim's name. * @param value the Claim's value. diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java index 120f6a9d..77d573bc 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java @@ -71,12 +71,12 @@ public Date asDate() { } @Override - public T[] asArray(Class tClazz) throws JWTDecodeException { + public T[] asArray(Class clazz) throws JWTDecodeException { return null; } @Override - public List asList(Class tClazz) throws JWTDecodeException { + public List asList(Class clazz) throws JWTDecodeException { return null; } From 0029a6363f89c1af43db64a76a002e8169c2656c Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 25 Mar 2022 16:25:50 +0530 Subject: [PATCH 175/293] [SDK-3155] Predicate based Claim verification (#562) * Rename claims -> expectedClaims in verifier for clarity * added method to validate claim value using predicate * Using Predicates for Verification (#560) * Use predicates for verification * Code review changes * Add additional tests for predicate based verification * Fixed Lint issues --- .../main/java/com/auth0/jwt/JWTVerifier.java | 366 ++++++++---------- .../auth0/jwt/interfaces/Verification.java | 12 + .../java/com/auth0/jwt/JWTVerifierTest.java | 207 +++++----- .../jwt/interfaces/VerificationTest.java | 6 + 4 files changed, 283 insertions(+), 308 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 77dfef4e..8b14ff86 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -14,6 +14,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.function.BiPredicate; /** * The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, @@ -25,17 +26,12 @@ */ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { private final Algorithm algorithm; - final Map claims; - private final Clock clock; + final Map> expectedChecks; private final JWTParser parser; - static final String AUDIENCE_EXACT = "AUDIENCE_EXACT"; - static final String AUDIENCE_CONTAINS = "AUDIENCE_CONTAINS"; - - JWTVerifier(Algorithm algorithm, Map claims, Clock clock) { + JWTVerifier(Algorithm algorithm, Map> expectedChecks) { this.algorithm = algorithm; - this.claims = Collections.unmodifiableMap(claims); - this.clock = clock; + this.expectedChecks = Collections.unmodifiableMap(expectedChecks); this.parser = new JWTParser(); } @@ -55,9 +51,11 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { */ public static class BaseVerification implements Verification { private final Algorithm algorithm; - private final Map claims; + private final Map> expectedChecks; private long defaultLeeway; + private final Map customLeeways; private boolean ignoreIssuedAt; + private Clock clock; BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { @@ -65,33 +63,42 @@ public static class BaseVerification implements Verification { } this.algorithm = algorithm; - this.claims = new HashMap<>(); + this.expectedChecks = new LinkedHashMap<>(); + this.customLeeways = new HashMap<>(); this.defaultLeeway = 0; } @Override public Verification withIssuer(String... issuer) { - requireClaim(PublicClaims.ISSUER, isNullOrEmpty(issuer) ? null : Arrays.asList(issuer)); + List value = isNullOrEmpty(issuer) ? null : Arrays.asList(issuer); + checkIfNeedToRemove(PublicClaims.ISSUER, value, ((claim, decodedJWT) -> { + if (value == null || !value.contains(claim.asString())) { + throw new InvalidClaimException("The Claim 'iss' value doesn't match the required issuer."); + } + return true; + })); return this; } @Override public Verification withSubject(String subject) { - requireClaim(PublicClaims.SUBJECT, subject); + checkIfNeedToRemove(PublicClaims.SUBJECT, subject, (claim, decodedJWT) -> subject.equals(claim.asString())); return this; } @Override public Verification withAudience(String... audience) { - claims.remove(AUDIENCE_CONTAINS); - requireClaim(AUDIENCE_EXACT, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); + checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> + assertValidAudienceClaim(decodedJWT.getAudience(), value, true))); return this; } @Override public Verification withAnyOfAudience(String... audience) { - claims.remove(AUDIENCE_EXACT); - requireClaim(AUDIENCE_CONTAINS, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); + checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> + assertValidAudienceClaim(decodedJWT.getAudience(), value, false))); return this; } @@ -105,21 +112,21 @@ public Verification acceptLeeway(long leeway) throws IllegalArgumentException { @Override public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.EXPIRES_AT, leeway); + customLeeways.put(PublicClaims.EXPIRES_AT, leeway); return this; } @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.NOT_BEFORE, leeway); + customLeeways.put(PublicClaims.NOT_BEFORE, leeway); return this; } @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.ISSUED_AT, leeway); + customLeeways.put(PublicClaims.ISSUED_AT, leeway); return this; } @@ -131,49 +138,54 @@ public Verification ignoreIssuedAt() { @Override public Verification withJWTId(String jwtId) { - requireClaim(PublicClaims.JWT_ID, jwtId); + checkIfNeedToRemove(PublicClaims.JWT_ID, jwtId, ((claim, decodedJWT) -> jwtId.equals(claim.asString()))); return this; } @Override public Verification withClaimPresence(String name) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, NonEmptyClaim.getInstance()); + withClaim(name, ((claim, decodedJWT) -> { + if (claim instanceof NullClaim) { + throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", name)); + } + return true; + })); return this; } @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asBoolean()))); return this; } @Override public Verification withClaim(String name, Integer value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asInt()))); return this; } @Override public Verification withClaim(String name, Long value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asLong()))); return this; } @Override public Verification withClaim(String name, Double value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asDouble()))); return this; } @Override public Verification withClaim(String name, String value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asString()))); return this; } @@ -187,28 +199,37 @@ public Verification withClaim(String name, Instant value) throws IllegalArgument assertNonNull(name); // Since date-time claims are serialized as epoch seconds, // we need to compare them with only seconds-granularity - requireClaim(name, value != null ? value.truncatedTo(ChronoUnit.SECONDS) : null); + checkIfNeedToRemove(name, value, + ((claim, decodedJWT) -> value.truncatedTo(ChronoUnit.SECONDS).equals(claim.asInstant()))); + return this; + } + + @Override + public Verification withClaim(String name, BiPredicate predicate) + throws IllegalArgumentException { + assertNonNull(name); + checkIfNeedToRemove(name, predicate, predicate); return this; } @Override public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, items); + checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); return this; } @Override public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, items); + checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); return this; } @Override public Verification withArrayClaim(String name, Long... items) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, items); + checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); return this; } @@ -225,59 +246,122 @@ public JWTVerifier build() { * @return a new JWTVerifier instance with a custom {@link java.time.Clock} */ public JWTVerifier build(Clock clock) { - addLeewayToDateClaims(); - return new JWTVerifier(algorithm, claims, clock); + this.clock = clock; + addMandatoryClaimChecks(); + return new JWTVerifier(algorithm, expectedChecks); } - private void assertPositive(long leeway) { - if (leeway < 0) { - throw new IllegalArgumentException("Leeway value can't be negative."); + /** + * Fetches the Leeway set for claim or returns the {@link BaseVerification#defaultLeeway}. + * + * @param name Claim for which leeway is fetched + * @return Leeway value set for the claim + */ + public long getLeewayFor(String name) { + return customLeeways.getOrDefault(name, defaultLeeway); + } + + private void addMandatoryClaimChecks() { + long expiresAtLeeway = getLeewayFor(PublicClaims.EXPIRES_AT); + long notBeforeLeeway = getLeewayFor(PublicClaims.NOT_BEFORE); + long issuedAtLeeway = getLeewayFor(PublicClaims.ISSUED_AT); + + checkIfNeedToRemove(PublicClaims.EXPIRES_AT, expiresAtLeeway, ((claim, decodedJWT) -> + assertValidInstantClaim(claim.asInstant(), expiresAtLeeway, true))); + checkIfNeedToRemove(PublicClaims.NOT_BEFORE, notBeforeLeeway, ((claim, decodedJWT) -> + assertValidInstantClaim(claim.asInstant(), notBeforeLeeway, false))); + if (!ignoreIssuedAt) { + checkIfNeedToRemove(PublicClaims.ISSUED_AT, issuedAtLeeway, ((claim, decodedJWT) -> + assertValidInstantClaim(claim.asInstant(), issuedAtLeeway, false))); } } - private void assertNonNull(String name) { - if (name == null) { - throw new IllegalArgumentException("The Custom Claim's name can't be null."); + private boolean assertValidCollectionClaim(Claim claim, Object[] expectedClaimValue) { + List claimArr; + Object[] claimAsObject = claim.as(Object[].class); + + // Jackson uses 'natural' mapping which uses Integer if value fits in 32 bits. + if (expectedClaimValue instanceof Long[]) { + // convert Integers to Longs for comparison with equals + claimArr = new ArrayList<>(claimAsObject.length); + for (Object cao : claimAsObject) { + if (cao instanceof Integer) { + claimArr.add(((Integer) cao).longValue()); + } else { + claimArr.add(cao); + } + } + } else { + claimArr = claim.isNull() ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); } + List valueArr = Arrays.asList(expectedClaimValue); + return claimArr.containsAll(valueArr); } - private void addLeewayToDateClaims() { - if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { - claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); + private boolean assertValidInstantClaim(Instant claimVal, long leeway, boolean shouldBeFuture) { + Instant now = clock.instant().truncatedTo(ChronoUnit.SECONDS); + if (shouldBeFuture) { + return assertInstantIsFuture(claimVal, leeway, now); + } else { + return assertInstantIsPast(claimVal, leeway, now); } - if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { - claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); + } + + private boolean assertInstantIsFuture(Instant claimVal, long leeway, Instant now) { + if (claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)) { + throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal)); } - if (ignoreIssuedAt) { - claims.remove(PublicClaims.ISSUED_AT); - return; + return true; + } + + private boolean assertInstantIsPast(Instant claimVal, long leeway, Instant now) { + if (claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)) { + throw new InvalidClaimException(String.format("The Token can't be used before %s.", claimVal)); + } + return true; + } + + private boolean assertValidAudienceClaim(List audience, List values, boolean shouldContainAll) { + if (audience == null || (shouldContainAll && !audience.containsAll(values)) + || (!shouldContainAll && Collections.disjoint(audience, values))) { + throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); + return true; + } + + private void assertPositive(long leeway) { + if (leeway < 0) { + throw new IllegalArgumentException("Leeway value can't be negative."); + } + } + + private void assertNonNull(String name) { + if (name == null) { + throw new IllegalArgumentException("The Custom Claim's name can't be null."); } } - private void requireClaim(String name, Object value) { + private void checkIfNeedToRemove(String name, Object value, BiPredicate predicate) { if (value == null) { - claims.remove(name); + expectedChecks.remove(name); return; } - claims.put(name, value); + expectedChecks.put(name, predicate); } - } - private static boolean isNullOrEmpty(String[] args) { - if (args == null || args.length == 0) { - return true; - } - boolean isAllNull = true; - for (String arg : args) { - if (arg != null) { - isAllNull = false; - break; + private boolean isNullOrEmpty(String[] args) { + if (args == null || args.length == 0) { + return true; + } + boolean isAllNull = true; + for (String arg : args) { + if (arg != null) { + isAllNull = false; + break; + } } + return isAllNull; } - return isAllNull; } @@ -313,7 +397,7 @@ public DecodedJWT verify(String token) throws JWTVerificationException { public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException { verifyAlgorithm(jwt, algorithm); algorithm.verify(jwt); - verifyClaims(jwt, claims); + verifyClaims(jwt, expectedChecks); return jwt; } @@ -324,156 +408,20 @@ private void verifyAlgorithm(DecodedJWT jwt, Algorithm expectedAlgorithm) throws } } - private void verifyClaims(DecodedJWT jwt, Map claims) + private void verifyClaims(DecodedJWT jwt, Map> claims) throws TokenExpiredException, InvalidClaimException { - for (Map.Entry entry : claims.entrySet()) { - if (entry.getValue() instanceof NonEmptyClaim) { - assertClaimPresent(jwt.getClaim(entry.getKey()), entry.getKey()); - } else { - verifyClaimValues(jwt, entry); - } - } - } - - private void verifyClaimValues(DecodedJWT jwt, Map.Entry expectedClaim) { - switch (expectedClaim.getKey()) { - // We use custom keys for audience in the expected claims to differentiate between - // validating that the audience contains all expected values, or validating that the audience contains - // at least one of the expected values. - case AUDIENCE_EXACT: - assertValidAudienceClaim(jwt.getAudience(), (List) expectedClaim.getValue(), true); - break; - case AUDIENCE_CONTAINS: - assertValidAudienceClaim(jwt.getAudience(), (List) expectedClaim.getValue(), false); - break; - case PublicClaims.EXPIRES_AT: - assertValidInstantClaim(jwt.getExpiresAtAsInstant(), (Long) expectedClaim.getValue(), true); - break; - case PublicClaims.ISSUED_AT: - assertValidInstantClaim(jwt.getIssuedAtAsInstant(), (Long) expectedClaim.getValue(), false); - break; - case PublicClaims.NOT_BEFORE: - assertValidInstantClaim(jwt.getNotBeforeAsInstant(), (Long) expectedClaim.getValue(), false); - break; - case PublicClaims.ISSUER: - assertValidIssuerClaim(jwt.getIssuer(), (List) expectedClaim.getValue()); - break; - case PublicClaims.JWT_ID: - assertValidStringClaim(expectedClaim.getKey(), jwt.getId(), (String) expectedClaim.getValue()); - break; - case PublicClaims.SUBJECT: - assertValidStringClaim(expectedClaim.getKey(), jwt.getSubject(), (String) expectedClaim.getValue()); - break; - default: - assertValidClaim(jwt.getClaim(expectedClaim.getKey()), expectedClaim.getKey(), - expectedClaim.getValue()); - break; - } - } - - private void assertClaimPresent(Claim claim, String claimName) { - if (claim instanceof NullClaim) { - throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", claimName)); - } - } - - private void assertValidClaim(Claim claim, String claimName, Object value) { - boolean isValid = false; - if (value instanceof String) { - isValid = value.equals(claim.asString()); - } else if (value instanceof Integer) { - isValid = value.equals(claim.asInt()); - } else if (value instanceof Long) { - isValid = value.equals(claim.asLong()); - } else if (value instanceof Boolean) { - isValid = value.equals(claim.asBoolean()); - } else if (value instanceof Double) { - isValid = value.equals(claim.asDouble()); - } else if (value instanceof Instant) { - isValid = value.equals(claim.asInstant()); - } else if (value instanceof Object[]) { - List claimArr; - Object[] claimAsObject = claim.as(Object[].class); - - // Jackson uses 'natural' mapping which uses Integer if value fits in 32 bits. - if (value instanceof Long[]) { - // convert Integers to Longs for comparison with equals - claimArr = new ArrayList<>(claimAsObject.length); - for (Object cao : claimAsObject) { - if (cao instanceof Integer) { - claimArr.add(((Integer) cao).longValue()); - } else { - claimArr.add(cao); - } - } - } else { - claimArr = claim.isNull() ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); - } - List valueArr = Arrays.asList((Object[]) value); - isValid = claimArr.containsAll(valueArr); - } - - if (!isValid) { - throw new InvalidClaimException( - String.format("The Claim '%s' value doesn't match the required one.", claimName)); - } - } - - private void assertValidStringClaim(String claimName, String value, String expectedValue) { - if (!expectedValue.equals(value)) { - throw new InvalidClaimException( - String.format("The Claim '%s' value doesn't match the required one.", claimName)); - } - } - - private void assertValidInstantClaim(Instant claimVal, long leeway, boolean shouldBeFuture) { - Instant now = clock.instant().truncatedTo(ChronoUnit.SECONDS); - if (shouldBeFuture) { - assertInstantIsFuture(claimVal, leeway, now); - } else { - assertInstantIsPast(claimVal, leeway, now); - } - } - - private void assertInstantIsFuture(Instant claimVal, long leeway, Instant now) { - if (claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)) { - throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal)); - } - } - - private void assertInstantIsPast(Instant claimVal, long leeway, Instant now) { - if (claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)) { - throw new InvalidClaimException(String.format("The Token can't be used before %s.", claimVal)); - } - } - - private void assertValidAudienceClaim(List audience, List values, boolean shouldContainAll) { - if (audience == null || (shouldContainAll && !audience.containsAll(values)) - || (!shouldContainAll && Collections.disjoint(audience, values))) { - throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); - } - } + for (Map.Entry> entry : claims.entrySet()) { + boolean isValid; + String claimName = entry.getKey(); + BiPredicate expectedCheck = entry.getValue(); + Claim claim = jwt.getClaim(claimName); - private void assertValidIssuerClaim(String issuer, List value) { - if (issuer == null || !value.contains(issuer)) { - throw new InvalidClaimException("The Claim 'iss' value doesn't match the required issuer."); - } - } - - /** - * Simple singleton used to mark that a claim should only be verified for presence. - */ - private static class NonEmptyClaim { - private static NonEmptyClaim nonEmptyClaim; - - private NonEmptyClaim() { - } + isValid = expectedCheck.test(claim, jwt); - public static NonEmptyClaim getInstance() { - if (nonEmptyClaim == null) { - nonEmptyClaim = new NonEmptyClaim(); + if (!isValid) { + throw new InvalidClaimException( + String.format("The Claim '%s' value doesn't match the required one.", claimName)); } - return nonEmptyClaim; } } } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 071b3e9c..ffe0b920 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -4,6 +4,7 @@ import java.time.Instant; import java.util.Date; +import java.util.function.BiPredicate; /** * Holds the Claims and claim-based configurations required for a JWT to be considered valid. @@ -205,6 +206,17 @@ default Verification withClaim(String name, Instant value) throws IllegalArgumen return withClaim(name, value != null ? Date.from(value) : null); } + /** + * Executes the predicate provided during the verification + * and passes the verification if the predicate returns true. + * + * @param name the Claim's name + * @param predicate the predicate check to be done. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ + Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException; + /** * Require a specific Array Claim to contain at least the given items. * diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 93a7bcd1..1d80d802 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -4,6 +4,8 @@ import com.auth0.jwt.exceptions.AlgorithmMismatchException; import com.auth0.jwt.exceptions.InvalidClaimException; import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.impl.PublicClaims; +import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; import org.junit.Rule; @@ -16,11 +18,11 @@ import java.time.ZoneId; import java.util.Collections; import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.function.BiPredicate; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.isNotNull; import static org.mockito.Mockito.mock; public class JWTVerifierTest { @@ -263,63 +265,29 @@ public void shouldRemoveAudienceWhenPassingNullReference() { .withAudience((String) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); verifier = JWTVerifier.init(algorithm) .withAudience((String[]) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); verifier = JWTVerifier.init(algorithm) .withAudience() .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_EXACT))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); String emptyAud = " "; verifier = JWTVerifier.init(algorithm) .withAudience(emptyAud) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_EXACT, Collections.singletonList(emptyAud))); - } - - @Test - public void shouldRemoveAudienceWhenPassingNullReferenceWithAnyOfAudience() { - Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience((String) null) - .build(); - - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); - - verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience((String[]) null) - .build(); - - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); - - verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience() - .build(); - - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey(JWTVerifier.AUDIENCE_CONTAINS))); - - String emptyAud = " "; - verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience(emptyAud) - .build(); - - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry(JWTVerifier.AUDIENCE_CONTAINS, Collections.singletonList(emptyAud))); + assertThat(verifier.expectedChecks, is(notNullValue())); } @Test @@ -330,16 +298,16 @@ public void shouldRemoveAudienceWhenPassingNull() { .withAudience((String) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("aud"))); verifier = JWTVerifier.init(algorithm) .withAudience("John") .withAudience((String[]) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("aud"))); } @Test @@ -350,16 +318,16 @@ public void shouldRemoveAudienceWhenPassingNullWithAnyAudience() { .withAnyOfAudience((String) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("aud"))); verifier = JWTVerifier.init(algorithm) .withAnyOfAudience("John") .withAnyOfAudience((String[]) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("aud"))); } @Test @@ -453,10 +421,10 @@ public void shouldThrowOnInvalidCustomClaimValue() { exception.expect(InvalidClaimException.class); exception.expectMessage("The Claim 'name' value doesn't match the required one."); String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - Map map = new HashMap<>(); - map.put("name", new Object()); - JWTVerifier verifier = new JWTVerifier(Algorithm.HMAC256("secret"), map, Clock.systemUTC()); - verifier.verify(token); + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", "check") + .build() + .verify(token); } @Test @@ -533,8 +501,8 @@ public void shouldRemoveCustomClaimOfTypeDateWhenNull() { .withClaim("name", (Date) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); } @Test @@ -593,76 +561,76 @@ public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() } // Generic Delta - @SuppressWarnings("RedundantCast") @Test public void shouldAddDefaultLeewayToDateClaims() { Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(algorithm); + JWTVerifier verifier = verification .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iat", (Object) 0L)); - assertThat(verifier.claims, hasEntry("exp", (Object) 0L)); - assertThat(verifier.claims, hasEntry("nbf", (Object) 0L)); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(0L)); + assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(0L)); + assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(0L)); } - @SuppressWarnings("RedundantCast") @Test public void shouldAddCustomLeewayToDateClaims() { Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(algorithm); + JWTVerifier verifier = verification .acceptLeeway(1234L) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iat", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("exp", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("nbf", (Object) 1234L)); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test public void shouldOverrideDefaultIssuedAtLeeway() { Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(algorithm); + JWTVerifier verifier = verification .acceptLeeway(1234L) .acceptIssuedAt(9999L) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iat", (Object) 9999L)); - assertThat(verifier.claims, hasEntry("exp", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("nbf", (Object) 1234L)); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(9999L)); + assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test public void shouldOverrideDefaultExpiresAtLeeway() { Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(algorithm); + JWTVerifier verifier = verification .acceptLeeway(1234L) .acceptExpiresAt(9999L) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iat", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("exp", (Object) 9999L)); - assertThat(verifier.claims, hasEntry("nbf", (Object) 1234L)); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(9999L)); + assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test public void shouldOverrideDefaultNotBeforeLeeway() { Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(algorithm); + JWTVerifier verifier = verification .acceptLeeway(1234L) .acceptNotBefore(9999L) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iat", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("exp", (Object) 1234L)); - assertThat(verifier.claims, hasEntry("nbf", (Object) 9999L)); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(9999L)); } @Test @@ -860,16 +828,16 @@ public void shouldRemoveClaimWhenPassingNull() { .withIssuer((String) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); verifier = JWTVerifier.init(algorithm) .withIssuer("iss") .withIssuer((String[]) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); } @Test @@ -879,30 +847,29 @@ public void shouldRemoveIssuerWhenPassingNullReference() { .withIssuer((String) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); verifier = JWTVerifier.init(algorithm) .withIssuer((String[]) null) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); verifier = JWTVerifier.init(algorithm) .withIssuer() .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("iss"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("iss"))); String emptyIss = " "; verifier = JWTVerifier.init(algorithm) .withIssuer(emptyIss) .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("iss", Collections.singletonList(emptyIss))); + assertThat(verifier.expectedChecks, is(notNullValue())); } @Test @@ -1055,4 +1022,46 @@ public void shouldVerifyStandardClaimPresence() { DecodedJWT decodedJWT = verifier.verify(jwt); assertThat(decodedJWT, is(notNullValue())); } + + @Test + public void shouldSuccessfullyVerifyClaimWithPredicate() { + String jwt = JWTCreator.init() + .withClaim("claimName", "claimValue") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("claimName", (claim, decodedJWT) -> "claimValue".equals(claim.asString())) + .withClaim(PublicClaims.ISSUED_AT, ((claim, decodedJWT) -> false)) + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldThrowWhenPredicateReturnsFalse() { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'claimName' value doesn't match the required one."); + + String jwt = JWTCreator.init() + .withClaim("claimName", "claimValue") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("claimName", (claim, decodedJWT) -> "nope".equals(claim.asString())) + .build() + .verify(jwt); + } + + @Test + public void shouldRemovePredicateCheckForNull() { + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("claimName", (claim, decodedJWT) -> "nope".equals(claim.asString())) + .withClaim("claimName", (BiPredicate) null) + .build(); + + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks, not(hasKey("claimName"))); + } + } diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java index e9f4f815..c7f89763 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java @@ -7,6 +7,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.function.BiPredicate; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasEntry; @@ -149,6 +150,11 @@ public Verification withArrayClaim(String name, Long... items) throws IllegalArg return null; } + @Override + public Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException { + return null; + } + @Override public Verification ignoreIssuedAt() { return null; From e37301aad52681174bcd2dc39844ab56408e7b8a Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 25 Mar 2022 16:38:56 +0530 Subject: [PATCH 176/293] [SDK-3158] Null claim handling (#564) * Handle claim difference between missing and null * Verification with null claims * JWT creation support for null values * Test cases for JWT verification and construction * Add JWT decode test cases * Fix broken tests * Fixed Lint issues * Fixed formatting errors * Add test case to check Claim toString conversion --- .../main/java/com/auth0/jwt/JWTCreator.java | 45 ++++-- .../main/java/com/auth0/jwt/JWTVerifier.java | 13 +- .../com/auth0/jwt/impl/JsonNodeClaim.java | 39 +++-- .../java/com/auth0/jwt/impl/NullClaim.java | 79 --------- .../java/com/auth0/jwt/interfaces/Claim.java | 10 ++ .../auth0/jwt/interfaces/Verification.java | 9 ++ .../java/com/auth0/jwt/JWTCreatorTest.java | 152 +++++++++--------- .../java/com/auth0/jwt/JWTDecoderTest.java | 21 ++- .../java/com/auth0/jwt/JWTVerifierTest.java | 43 +++++ .../com/auth0/jwt/impl/BasicHeaderTest.java | 3 +- .../com/auth0/jwt/impl/JsonNodeClaimTest.java | 45 +++++- .../com/auth0/jwt/impl/NullClaimTest.java | 82 ---------- .../com/auth0/jwt/impl/PayloadImplTest.java | 3 +- .../com/auth0/jwt/interfaces/ClaimTest.java | 5 + .../jwt/interfaces/VerificationTest.java | 5 + 15 files changed, 273 insertions(+), 281 deletions(-) delete mode 100644 lib/src/main/java/com/auth0/jwt/impl/NullClaim.java delete mode 100644 lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 6b355b36..e1159ae4 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -75,7 +75,6 @@ public static class Builder { /** * Add specific Claims to set as the Header. * If provided map is null then nothing is changed - * If provided map contains a claim with null value then that claim will be removed from the header * * @param headerClaims the values to use as Claims in the token's Header. * @return this same Builder instance. @@ -362,7 +361,6 @@ public Builder withClaim(String name, Map map) throws IllegalArgument * @return this same Builder instance. * @throws IllegalArgumentException if the name is null, or if the list contents does not validate. */ - public Builder withClaim(String name, List list) throws IllegalArgumentException { assertNonNull(name); // validate list contents @@ -374,6 +372,19 @@ public Builder withClaim(String name, List list) throws IllegalArgumentExcept return this; } + /** + * Add a custom claim with null value. + * + * @param name the Claim's name. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null + */ + public Builder withNullClaim(String name) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, null); + return this; + } + /** * Add a custom Array Claim with the given items. * @@ -422,8 +433,8 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE *

* Accepted types are {@linkplain Map} and {@linkplain List} with basic types * {@linkplain Boolean}, {@linkplain Integer}, {@linkplain Long}, {@linkplain Double}, - * {@linkplain String} and {@linkplain Date}. {@linkplain Map}s cannot contain null keys or values. - * {@linkplain List}s can contain null elements. + * {@linkplain String} and {@linkplain Date}. + * {@linkplain Map}s and {@linkplain List}s can contain null elements. *

* *

@@ -442,7 +453,7 @@ public Builder withPayload(Map payloadClaims) throws IllegalArgumentE if (!validatePayload(payloadClaims)) { throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, " - + "Long, Double, String and Date"); + + "Long, Double, String, Date and Null"); } // add claims only after validating all claims so as not to corrupt the claims map of this builder @@ -463,7 +474,7 @@ private boolean validatePayload(Map payload) { return false; } else if (value instanceof Map && !validateClaim((Map) value)) { return false; - } else if (value != null && !isSupportedType(value)) { + } else if (!isSupportedType(value)) { return false; } } @@ -474,7 +485,7 @@ private static boolean validateClaim(Map map) { // do not accept null values in maps for (Entry entry : map.entrySet()) { Object value = entry.getValue(); - if (value == null || !isSupportedType(value)) { + if (!isSupportedType(value)) { return false; } @@ -488,7 +499,7 @@ private static boolean validateClaim(Map map) { private static boolean validateClaim(List list) { // accept null values in list for (Object object : list) { - if (object != null && !isSupportedType(object)) { + if (!isSupportedType(object)) { return false; } } @@ -506,13 +517,17 @@ private static boolean isSupportedType(Object value) { } private static boolean isBasicType(Object value) { - Class c = value.getClass(); + if (value == null) { + return true; + } else { + Class c = value.getClass(); - if (c.isArray()) { - return c == Integer[].class || c == Long[].class || c == String[].class; + if (c.isArray()) { + return c == Integer[].class || c == Long[].class || c == String[].class; + } + return c == String.class || c == Integer.class || c == Long.class || c == Double.class + || c == Date.class || c == Instant.class || c == Boolean.class; } - return c == String.class || c == Integer.class || c == Long.class || c == Double.class - || c == Date.class || c == Instant.class || c == Boolean.class; } /** @@ -546,10 +561,6 @@ private void assertNonNull(String name) { } private void addClaim(String name, Object value) { - if (value == null) { - payloadClaims.remove(name); - return; - } payloadClaims.put(name, value); } } diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 8b14ff86..947c9e25 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -3,7 +3,6 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; import com.auth0.jwt.impl.JWTParser; -import com.auth0.jwt.impl.NullClaim; import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; @@ -146,7 +145,7 @@ public Verification withJWTId(String jwtId) { public Verification withClaimPresence(String name) throws IllegalArgumentException { assertNonNull(name); withClaim(name, ((claim, decodedJWT) -> { - if (claim instanceof NullClaim) { + if (claim.isMissing()) { throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", name)); } return true; @@ -154,6 +153,13 @@ public Verification withClaimPresence(String name) throws IllegalArgumentExcepti return this; } + @Override + public Verification withNullClaim(String name) throws IllegalArgumentException { + assertNonNull(name); + withClaim(name, ((claim, decodedJWT) -> claim.isNull())); + return this; + } + @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); @@ -292,7 +298,8 @@ private boolean assertValidCollectionClaim(Claim claim, Object[] expectedClaimVa } } } else { - claimArr = claim.isNull() ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); + claimArr = claim.isNull() || claim.isMissing() + ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); } List valueArr = Arrays.asList(expectedClaimValue); return claimArr.containsAll(valueArr); diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 54a09575..5bb3dbbc 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -31,32 +31,32 @@ private JsonNodeClaim(JsonNode node, ObjectReader objectReader) { @Override public Boolean asBoolean() { - return !data.isBoolean() ? null : data.asBoolean(); + return isMissing() || isNull() || !data.isBoolean() ? null : data.asBoolean(); } @Override public Integer asInt() { - return !data.isNumber() ? null : data.asInt(); + return isMissing() || isNull() || !data.isNumber() ? null : data.asInt(); } @Override public Long asLong() { - return !data.isNumber() ? null : data.asLong(); + return isMissing() || isNull() || !data.isNumber() ? null : data.asLong(); } @Override public Double asDouble() { - return !data.isNumber() ? null : data.asDouble(); + return isMissing() || isNull() || !data.isNumber() ? null : data.asDouble(); } @Override public String asString() { - return !data.isTextual() ? null : data.asText(); + return isMissing() || isNull() || !data.isTextual() ? null : data.asText(); } @Override public Date asDate() { - if (!data.canConvertToLong()) { + if (isMissing() || isNull() || !data.canConvertToLong()) { return null; } long seconds = data.asLong(); @@ -65,7 +65,7 @@ public Date asDate() { @Override public Instant asInstant() { - if (!data.canConvertToLong()) { + if (isMissing() || isNull() || !data.canConvertToLong()) { return null; } long seconds = data.asLong(); @@ -75,7 +75,7 @@ public Instant asInstant() { @Override @SuppressWarnings("unchecked") public T[] asArray(Class clazz) throws JWTDecodeException { - if (!data.isArray()) { + if (isMissing() || isNull() || !data.isArray()) { return null; } @@ -92,7 +92,7 @@ public T[] asArray(Class clazz) throws JWTDecodeException { @Override public List asList(Class clazz) throws JWTDecodeException { - if (!data.isArray()) { + if (isMissing() || isNull() || !data.isArray()) { return null; } @@ -109,7 +109,7 @@ public List asList(Class clazz) throws JWTDecodeException { @Override public Map asMap() throws JWTDecodeException { - if (!data.isObject()) { + if (isMissing() || isNull() || !data.isObject()) { return null; } @@ -126,6 +126,9 @@ public Map asMap() throws JWTDecodeException { @Override public T as(Class clazz) throws JWTDecodeException { try { + if (isMissing() || isNull()) { + return null; + } return objectReader.treeAsTokens(data).readValueAs(clazz); } catch (IOException e) { throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e); @@ -134,11 +137,21 @@ public T as(Class clazz) throws JWTDecodeException { @Override public boolean isNull() { - return false; + return !isMissing() && data.isNull(); + } + + @Override + public boolean isMissing() { + return data == null || data.isMissingNode(); } @Override public String toString() { + if (isMissing()) { + return "Missing claim"; + } else if (isNull()) { + return "Null claim"; + } return data.toString(); } @@ -161,10 +174,8 @@ static Claim extractClaim(String claimName, Map tree, ObjectRe * @return a valid Claim instance. If the node is null or missing, a NullClaim will be returned. */ static Claim claimFromNode(JsonNode node, ObjectReader objectReader) { - if (node == null || node.isNull() || node.isMissingNode()) { - return new NullClaim(); - } return new JsonNodeClaim(node, objectReader); } } +//todo test all as* methods in JsonNodeClaim to ensure isMissing isNull calls are made \ No newline at end of file diff --git a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java deleted file mode 100644 index 2975b51b..00000000 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.auth0.jwt.impl; - -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.Claim; - -import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * The {@code NullClaim} class is a Claim implementation that returns null when any of it's methods is called. - */ -public class NullClaim implements Claim { - @Override - public boolean isNull() { - return true; - } - - @Override - public Boolean asBoolean() { - return null; - } - - @Override - public Integer asInt() { - return null; - } - - @Override - public Long asLong() { - return null; - } - - @Override - public Double asDouble() { - return null; - } - - @Override - public String asString() { - return null; - } - - @Override - public Date asDate() { - return null; - } - - @Override - public Instant asInstant() { - return null; - } - - @Override - public T[] asArray(Class clazz) throws JWTDecodeException { - return null; - } - - @Override - public List asList(Class clazz) throws JWTDecodeException { - return null; - } - - @Override - public Map asMap() throws JWTDecodeException { - return null; - } - - @Override - public T as(Class clazz) throws JWTDecodeException { - return null; - } - - @Override - public String toString() { - return "Null Claim"; - } -} diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index fdfac715..f6ccfdcd 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -14,11 +14,20 @@ public interface Claim { /** * Whether this Claim has a null value or not. + * If the claim is not present, it will return false hence checking {@link Claim#isMissing} is advised as well * * @return whether this Claim has a null value or not. */ boolean isNull(); + /** + * Can be used to verify whether the Claim is found or not. + * This will be true even if the Claim has null value associated to it. + * + * @return whether this Claim is present or not + */ + boolean isMissing(); + /** * Get this Claim as a Boolean. * If the value isn't of type Boolean or it can't be converted to a Boolean, null will be returned. @@ -110,6 +119,7 @@ default Instant asInstant() { /** * Get this Claim as a custom type T. + * This method will return null if {@link Claim#isMissing()} or {@link Claim#isNull()} is true * * @param type * @param clazz the type class diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index ffe0b920..8b0416dc 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -132,6 +132,15 @@ default Verification withAnyOfAudience(String... audience) { */ Verification withClaimPresence(String name) throws IllegalArgumentException; + /** + * Require a specific Claim value to be null. + * + * @param name the Claim's name. + * @return this same Verification instance. + * @throws IllegalArgumentException if the name is null. + */ + Verification withNullClaim(String name) throws IllegalArgumentException; + /** * Require a specific Claim value. * diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 6c870656..3cccbf7b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -326,17 +326,6 @@ public void shouldAddJWTId() { assertThat(TokenUtils.splitToken(signed)[1], is("eyJqdGkiOiJqd3RfaWRfMTIzIn0")); } - @Test - public void shouldRemoveClaimWhenPassingNull() { - String signed = JWTCreator.init() - .withIssuer("iss") - .withIssuer(null) - .sign(Algorithm.HMAC256("secret")); - - assertThat(signed, is(notNullValue())); - assertThat(TokenUtils.splitToken(signed)[1], is("e30")); - } - @Test public void shouldSetCorrectAlgorithmInTheHeader() { String signed = JWTCreator.init() @@ -615,7 +604,7 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); - + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); ObjectMapper mapper = new ObjectMapper(); List list = (List) mapper.readValue(body, Map.class).get("data"); @@ -648,52 +637,6 @@ public void shouldAcceptCustomClaimForNullListItem() { .sign(Algorithm.HMAC256("secret")); } - @Test - @SuppressWarnings("unchecked") - public void shouldAcceptCustomClaimWithNullMapAndRemoveClaim() throws Exception { - String jwt = JWTCreator.init() - .withClaim("map", "stubValue") - .withClaim("map", (Map) null) - .sign(Algorithm.HMAC256("secret")); - - assertThat(jwt, is(notNullValue())); - String[] parts = jwt.split("\\."); - - String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - ObjectMapper mapper = new ObjectMapper(); - Map map = (Map) mapper.readValue(body, Map.class); - assertThat(map, anEmptyMap()); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldAcceptCustomClaimWithNullListAndRemoveClaim() throws Exception { - String jwt = JWTCreator.init() - .withClaim("list", "stubValue") - .withClaim("list", (List) null) - .sign(Algorithm.HMAC256("secret")); - - assertThat(jwt, is(notNullValue())); - String[] parts = jwt.split("\\."); - - String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - ObjectMapper mapper = new ObjectMapper(); - Map map = (Map) mapper.readValue(body, Map.class); - assertThat(map, anEmptyMap()); - } - - @Test - public void shouldRefuseCustomClaimForNullMapValue() { - Map data = new HashMap<>(); - data.put("subKey", null); - - exception.expect(IllegalArgumentException.class); - - JWTCreator.init() - .withClaim("pojo", data) - .sign(Algorithm.HMAC256("secret")); - } - @Test public void shouldRefuseCustomClaimForNullMapKey() { Map data = new HashMap<>(); @@ -814,26 +757,10 @@ public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.ISSUER, "abc")); } - @Test - public void shouldRemovePayloadIfTheValueIsNull() throws Exception { - String jwt = JWTCreator.init() - .withClaim("key", "stubValue") - .withPayload(Collections.singletonMap("key", (Map) null)) - .sign(Algorithm.HMAC256("secret")); - - assertThat(jwt, is(notNullValue())); - String[] parts = jwt.split("\\."); - - String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - ObjectMapper mapper = new ObjectMapper(); - Map map = (Map) mapper.readValue(body, Map.class); - assertThat(map, anEmptyMap()); - } - @Test public void withPayloadShouldNotAllowCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); Map payload = new HashMap<>(); payload.put("entry", "value"); @@ -860,7 +787,7 @@ public void withPayloadShouldAllowNullListItems() { @Test public void withPayloadShouldNotAllowListWithCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); Map payload = new HashMap<>(); payload.put("list", Arrays.asList("item1", new UserPojo("name", 42))); @@ -872,7 +799,7 @@ public void withPayloadShouldNotAllowListWithCustomType() { @Test public void withPayloadShouldNotAllowMapWithCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String and Date"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); Map payload = new HashMap<>(); payload.put("entry", "value"); @@ -922,4 +849,75 @@ public void withPayloadShouldAllowNestedSupportedTypes() { assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim)); assertThat(payloadJson, JsonMatcher.hasEntry("objClaim", mapClaim)); } + + @Test + public void withPayloadShouldSupportNullValuesEverywhere() { + /* + JWT: + { + "listClaim": [ + "answer to ultimate question of life", + 42, + null + ], + "claim": null, + "listNestedClaim": [ + 1, + 2, + { + "nestedObjKey": null + } + ], + "objClaim": { + "nestedObjKey": null, + "objObjKey": { + "nestedObjKey": null, + "objListKey": [ + null, + "nestedList2" + ] + }, + "objListKey": [ + null, + "nestedList2" + ] + } + } + */ + + List listClaim = Arrays.asList("answer to ultimate question of life", 42, null); + List listNestedClaim = Arrays.asList(1, 2, Collections.singletonMap("nestedObjKey", null)); + List objListKey = Arrays.asList(null, "nestedList2"); + HashMap objClaim = new HashMap<>(); + objClaim.put("nestedObjKey", null); + objClaim.put("objListKey", objListKey); + objClaim.put("objObjKey", new HashMap<>(objClaim)); + + + Map payload = new HashMap<>(); + payload.put("claim", null); + payload.put("listClaim", listClaim); + payload.put("listNestedClaim", listNestedClaim); + payload.put("objClaim", objClaim); + + String jwt = JWTCreator.init() + .withPayload(payload) + .withHeader(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + + assertThat(payloadJson, JsonMatcher.hasEntry("claim", null)); + assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim)); + assertThat(payloadJson, JsonMatcher.hasEntry("listNestedClaim", listNestedClaim)); + assertThat(payloadJson, JsonMatcher.hasEntry("objClaim", objClaim)); + + assertThat(headerJson, JsonMatcher.hasEntry("claim", null)); + assertThat(headerJson, JsonMatcher.hasEntry("listClaim", listClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("listNestedClaim", listNestedClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("objClaim", objClaim)); + } } diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index 32db703e..5ea2b4ef 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -1,7 +1,6 @@ package com.auth0.jwt; import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.impl.NullClaim; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import org.hamcrest.collection.IsCollectionWithSize; @@ -216,7 +215,8 @@ public void shouldGetMissingClaimIfClaimDoesNotExist() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.K17vlwhE8FCMShdl1_65jEYqsQqBOVMPUU9IgG-QlTM"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaim("notExisting"), is(notNullValue())); - assertThat(jwt.getClaim("notExisting"), is(instanceOf(NullClaim.class))); + assertThat(jwt.getClaim("notExisting").isMissing(), is(true)); + assertThat(jwt.getClaim("notExisting").isNull(), is(false)); } @Test @@ -295,13 +295,28 @@ public void shouldGetCustomArrayClaimOfTypeInteger() { @Test public void shouldGetCustomMapClaim() { - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp7InN0cmluZyI6InZhbHVlIiwibnVtYmVyIjoxLCJib29sZWFuIjp0cnVlfX0.-8aIaXd2-rp1lLuDEQmCeisCBX9X_zbqdPn2llGxNoc"; + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp7InN0cmluZyI6InZhbHVlIiwibnVtYmVyIjoxLCJib29sZWFuIjp0cnVlLCJlbXB0eSI6bnVsbH19.6xkCuYZnu4RA0xZSxlYSYAqzy9JDWsDtIWqSCUZlPt8"; DecodedJWT jwt = JWT.decode(token); assertThat(jwt, is(notNullValue())); Map map = jwt.getClaim("name").asMap(); assertThat(map, hasEntry("string", "value")); assertThat(map, hasEntry("number", 1)); assertThat(map, hasEntry("boolean", true)); + assertThat(map, hasEntry("empty", null)); + } + + @Test + public void shouldGetCustomNullClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpudWxsfQ.X4ALHe7uYqEcXWFBnwBUNRKwmwrtDEGZ2aynRYYUx8c"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt.getClaim("name").isNull(), is(true)); + } + + @Test + public void shouldGetListClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbbnVsbCwiaGVsbG8iXX0.SpcuQRBGdTV0ofHdxBSnhWEUsQi89noZUXin2Thwb70"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt.getClaim("name").asList(String.class), contains(null, "hello")); } @Test diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 1d80d802..7d2123a9 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -1064,4 +1064,47 @@ public void shouldRemovePredicateCheckForNull() { assertThat(verifier.expectedChecks, not(hasKey("claimName"))); } + @Test + public void shouldSuccessfullyVerifyClaimWithNull() { + String jwt = JWTCreator.init() + .withNullClaim("claimName") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withNullClaim("claimName") + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldThrowWhenNullClaimHasValue() { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'claimName' value doesn't match the required one."); + + String jwt = JWTCreator.init() + .withClaim("claimName", "value") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withNullClaim("claimName") + .build() + .verify(jwt); + } + + @Test + public void shouldThrowWhenNullClaimIsMissing() { + exception.expect(InvalidClaimException.class); + exception.expectMessage("The Claim 'anotherClaimName' value doesn't match the required one."); + + String jwt = JWTCreator.init() + .withClaim("claimName", "value") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withNullClaim("anotherClaimName") + .build() + .verify(jwt); + } } diff --git a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java index e3d04c77..c4a04d81 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java @@ -135,6 +135,7 @@ public void shouldGetNotNullExtraClaimIfMissing() { assertThat(header, is(notNullValue())); assertThat(header.getHeaderClaim("missing"), is(notNullValue())); - assertThat(header.getHeaderClaim("missing"), is(instanceOf(NullClaim.class))); + assertThat(header.getHeaderClaim("missing").isMissing(), is(true)); + assertThat(header.getHeaderClaim("missing").isNull(), is(false)); } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java index 3c744487..44ef2b9f 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -331,8 +331,8 @@ public void shouldReturnBaseClaimWhenParsingMissingNode() { Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); - assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(true)); + assertThat(claim.isNull(), is(false)); } @Test @@ -341,8 +341,8 @@ public void shouldReturnBaseClaimWhenParsingNullNode() { Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -351,8 +351,8 @@ public void shouldReturnBaseClaimWhenParsingNullValue() { Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -363,6 +363,7 @@ public void shouldReturnNonNullClaimWhenParsingObject() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -373,6 +374,7 @@ public void shouldReturnNonNullClaimWhenParsingArray() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -383,6 +385,7 @@ public void shouldReturnNonNullClaimWhenParsingList() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -393,6 +396,7 @@ public void shouldReturnNonNullClaimWhenParsingStringValue() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -403,6 +407,7 @@ public void shouldReturnNonNullClaimWhenParsingIntValue() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -413,6 +418,7 @@ public void shouldReturnNonNullClaimWhenParsingDoubleValue() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -423,6 +429,7 @@ public void shouldReturnNonNullClaimWhenParsingDateValue() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -433,6 +440,18 @@ public void shouldReturnNonNullClaimWhenParsingBooleanValue() { assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); + } + + @Test + public void shouldReturnNullIsTrue() { + JsonNode value = mapper.valueToTree(null); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + assertThat(claim, is(instanceOf(JsonNodeClaim.class))); + assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(false)); } @Test @@ -441,4 +460,22 @@ public void shouldDelegateToJsonNodeToString() { Claim claim = claimFromNode(value); assertThat(claim.toString(), is(value.toString())); } + + @Test + public void shouldConvertToString() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + JsonNode nullValue = mapper.valueToTree(null); + JsonNode missingValue = MissingNode.getInstance(); + + Claim claim = claimFromNode(value); + Claim nullClaim = claimFromNode(nullValue); + Claim missingClaim = claimFromNode(missingValue); + + assertThat(claim.toString(), is("{\"name\":\"john\",\"id\":123}")); + assertThat(nullClaim.isNull(), is(true)); + assertThat(nullClaim.toString(), is("Null claim")); + assertThat(missingClaim.isMissing(), is(true)); + assertThat(missingClaim.toString(), is("Missing claim")); + + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java deleted file mode 100644 index bd478130..00000000 --- a/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.auth0.jwt.impl; - -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -public class NullClaimTest { - private NullClaim claim; - - @Before - public void setUp() { - claim = new NullClaim(); - } - - @Test - public void shouldBeNull() { - assertThat(claim.isNull(), is(true)); - } - - @Test - public void shouldGetAsBoolean() { - assertThat(claim.asBoolean(), is(nullValue())); - } - - @Test - public void shouldGetAsInt() { - assertThat(claim.asInt(), is(nullValue())); - } - - @Test - public void shouldGetAsLong() { - assertThat(claim.asLong(), is(nullValue())); - } - - @Test - public void shouldGetAsDouble() { - assertThat(claim.asDouble(), is(nullValue())); - } - - @Test - public void shouldGetAsString() { - assertThat(claim.asString(), is(nullValue())); - } - - @Test - public void shouldGetAsDate() { - assertThat(claim.asDate(), is(nullValue())); - } - - @Test - public void shouldGetAsInstant() { - assertThat(claim.asInstant(), is(nullValue())); - } - - @Test - public void shouldGetAsArray() { - assertThat(claim.asArray(Object.class), is(nullValue())); - } - - @Test - public void shouldGetAsList() { - assertThat(claim.asList(Object.class), is(nullValue())); - } - - @Test - public void shouldGetAsMap() { - assertThat(claim.asMap(), is(nullValue())); - } - - @Test - public void shouldGetAsCustomClass() { - assertThat(claim.as(Object.class), is(nullValue())); - } - - @Test - public void shouldHaveToString() { - assertThat(claim.toString(), is("Null Claim")); - } -} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java index 9e2e6902..da0c880e 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -166,7 +166,8 @@ public void shouldGetNotNullExtraClaimIfMissing() { PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); assertThat(payload, is(notNullValue())); assertThat(payload.getClaim("missing"), is(notNullValue())); - assertThat(payload.getClaim("missing"), is(instanceOf(NullClaim.class))); + assertThat(payload.getClaim("missing").isMissing(), is(true)); + assertThat(payload.getClaim("missing").isNull(), is(false)); } @Test diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java index 77d573bc..61541ccf 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java @@ -40,6 +40,11 @@ public boolean isNull() { return false; } + @Override + public boolean isMissing() { + return false; + } + @Override public Boolean asBoolean() { return null; diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java index c7f89763..fb0c74a9 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java @@ -104,6 +104,11 @@ public Verification withClaimPresence(String name) throws IllegalArgumentExcepti return null; } + @Override + public Verification withNullClaim(String name) throws IllegalArgumentException { + return null; + } + @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { return null; From 67df3d476024cbe697b3a54893f5383945c7670d Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 29 Mar 2022 13:08:11 +0530 Subject: [PATCH 177/293] [SDK-3154] Improved Exception Handling (#568) * Improved exception handling * Fixed issues thrown by Linter * Fixed Lint Issue (Missing Period) * Improved Code Coverage --- .../main/java/com/auth0/jwt/JWTVerifier.java | 85 ++++++---- .../exceptions/IncorrectClaimException.java | 44 +++++ .../jwt/exceptions/InvalidClaimException.java | 2 +- .../jwt/exceptions/MissingClaimException.java | 23 +++ .../jwt/exceptions/TokenExpiredException.java | 11 +- .../com/auth0/jwt/impl/JsonNodeClaim.java | 3 +- .../java/com/auth0/jwt/JWTVerifierTest.java | 159 ++++++++++++++---- .../com/auth0/jwt/impl/JsonNodeClaimTest.java | 43 +++++ .../auth0/jwt/matchers/CustomMatchers.java | 114 +++++++++++++ 9 files changed, 410 insertions(+), 74 deletions(-) create mode 100644 lib/src/main/java/com/auth0/jwt/exceptions/IncorrectClaimException.java create mode 100644 lib/src/main/java/com/auth0/jwt/exceptions/MissingClaimException.java create mode 100644 lib/src/test/java/com/auth0/jwt/matchers/CustomMatchers.java diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 947c9e25..ba19feb9 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -72,7 +72,8 @@ public Verification withIssuer(String... issuer) { List value = isNullOrEmpty(issuer) ? null : Arrays.asList(issuer); checkIfNeedToRemove(PublicClaims.ISSUER, value, ((claim, decodedJWT) -> { if (value == null || !value.contains(claim.asString())) { - throw new InvalidClaimException("The Claim 'iss' value doesn't match the required issuer."); + throw new IncorrectClaimException( + "The Claim 'iss' value doesn't match the required issuer.", PublicClaims.ISSUER, claim); } return true; })); @@ -89,7 +90,7 @@ public Verification withSubject(String subject) { public Verification withAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> - assertValidAudienceClaim(decodedJWT.getAudience(), value, true))); + assertValidAudienceClaim(claim, decodedJWT.getAudience(), value, true))); return this; } @@ -97,7 +98,7 @@ public Verification withAudience(String... audience) { public Verification withAnyOfAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> - assertValidAudienceClaim(decodedJWT.getAudience(), value, false))); + assertValidAudienceClaim(claim, decodedJWT.getAudience(), value, false))); return this; } @@ -144,12 +145,7 @@ public Verification withJWTId(String jwtId) { @Override public Verification withClaimPresence(String name) throws IllegalArgumentException { assertNonNull(name); - withClaim(name, ((claim, decodedJWT) -> { - if (claim.isMissing()) { - throw new InvalidClaimException(String.format("The Claim '%s' is not present in the JWT.", name)); - } - return true; - })); + withClaim(name, ((claim, decodedJWT) -> assertClaimPresence(name, claim))); return this; } @@ -272,13 +268,13 @@ private void addMandatoryClaimChecks() { long notBeforeLeeway = getLeewayFor(PublicClaims.NOT_BEFORE); long issuedAtLeeway = getLeewayFor(PublicClaims.ISSUED_AT); - checkIfNeedToRemove(PublicClaims.EXPIRES_AT, expiresAtLeeway, ((claim, decodedJWT) -> - assertValidInstantClaim(claim.asInstant(), expiresAtLeeway, true))); - checkIfNeedToRemove(PublicClaims.NOT_BEFORE, notBeforeLeeway, ((claim, decodedJWT) -> - assertValidInstantClaim(claim.asInstant(), notBeforeLeeway, false))); + expectedChecks.put(PublicClaims.EXPIRES_AT, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.EXPIRES_AT, claim, expiresAtLeeway, true)); + expectedChecks.put(PublicClaims.NOT_BEFORE, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.NOT_BEFORE, claim, notBeforeLeeway, false)); if (!ignoreIssuedAt) { - checkIfNeedToRemove(PublicClaims.ISSUED_AT, issuedAtLeeway, ((claim, decodedJWT) -> - assertValidInstantClaim(claim.asInstant(), issuedAtLeeway, false))); + expectedChecks.put(PublicClaims.ISSUED_AT, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.ISSUED_AT, claim, issuedAtLeeway, false)); } } @@ -305,33 +301,50 @@ private boolean assertValidCollectionClaim(Claim claim, Object[] expectedClaimVa return claimArr.containsAll(valueArr); } - private boolean assertValidInstantClaim(Instant claimVal, long leeway, boolean shouldBeFuture) { + private boolean assertValidInstantClaim(String claimName, Claim claim, long leeway, boolean shouldBeFuture) { + Instant claimVal = claim.asInstant(); Instant now = clock.instant().truncatedTo(ChronoUnit.SECONDS); + boolean isValid; if (shouldBeFuture) { - return assertInstantIsFuture(claimVal, leeway, now); + isValid = assertInstantIsFuture(claimVal, leeway, now); + if (!isValid) { + throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal), claimVal); + } } else { - return assertInstantIsPast(claimVal, leeway, now); + isValid = assertInstantIsPast(claimVal, leeway, now); + if (!isValid) { + throw new IncorrectClaimException( + String.format("The Token can't be used before %s.", claimVal), claimName, claim); + } } + return true; } private boolean assertInstantIsFuture(Instant claimVal, long leeway, Instant now) { - if (claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)) { - throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal)); - } - return true; + return !(claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)); } private boolean assertInstantIsPast(Instant claimVal, long leeway, Instant now) { - if (claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)) { - throw new InvalidClaimException(String.format("The Token can't be used before %s.", claimVal)); - } - return true; + return !(claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)); } - private boolean assertValidAudienceClaim(List audience, List values, boolean shouldContainAll) { + private boolean assertValidAudienceClaim( + Claim claim, + List audience, + List values, + boolean shouldContainAll + ) { if (audience == null || (shouldContainAll && !audience.containsAll(values)) || (!shouldContainAll && Collections.disjoint(audience, values))) { - throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience."); + throw new IncorrectClaimException( + "The Claim 'aud' value doesn't contain the required audience.", PublicClaims.AUDIENCE, claim); + } + return true; + } + + private boolean assertClaimPresence(String name, Claim claim) { + if (claim.isMissing()) { + throw new MissingClaimException(name); } return true; } @@ -353,7 +366,8 @@ private void checkIfNeedToRemove(String name, Object value, BiPredicate assertClaimPresence(name, claim) + && predicate.test(claim, decodedJWT)); } private boolean isNullOrEmpty(String[] args) { @@ -381,7 +395,8 @@ private boolean isNullOrEmpty(String[] args) { * the one defined in the {@link JWTVerifier}. * @throws SignatureVerificationException if the signature is invalid. * @throws TokenExpiredException if the token has expired. - * @throws InvalidClaimException if a claim contained a different value than the expected one. + * @throws MissingClaimException if a claim to be verified is missing. + * @throws IncorrectClaimException if a claim contained a different value than the expected one. */ @Override public DecodedJWT verify(String token) throws JWTVerificationException { @@ -398,7 +413,8 @@ public DecodedJWT verify(String token) throws JWTVerificationException { * the one defined in the {@link JWTVerifier}. * @throws SignatureVerificationException if the signature is invalid. * @throws TokenExpiredException if the token has expired. - * @throws InvalidClaimException if a claim contained a different value than the expected one. + * @throws MissingClaimException if a claim to be verified is missing. + * @throws IncorrectClaimException if a claim contained a different value than the expected one. */ @Override public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException { @@ -426,8 +442,11 @@ private void verifyClaims(DecodedJWT jwt, Map hasMissingClaimName(final String claimName) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("MissingClaimException with claim name: "+claimName); + } + + @Override + protected boolean matchesSafely(MissingClaimException item) { + return item.getClaimName().equals(claimName); + } + }; + } + + public static Matcher hasTokenExpiredOn(final Instant instant) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("TokenExpiredException with expired time as: "+instant.getEpochSecond()); + } + + @Override + protected boolean matchesSafely(TokenExpiredException item) { + return item.getExpiredOn().equals(instant); + } + }; + } + + public static Matcher hasClaimName(final String claimName) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("IncorrectClaimException with claim name: "+claimName); + } + + @Override + protected boolean matchesSafely(IncorrectClaimException item) { + return item.getClaimName().equals(claimName); + } + }; + } + + public static Matcher hasClaimValue(final Object value, final Class clazz) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("IncorrectClaimException with claim : "+value); + } + + @Override + protected boolean matchesSafely(IncorrectClaimException item) { + return item.getClaimValue().as(clazz).equals(value); + } + }; + } + + public static Matcher hasClaimInstant(final Instant value, final Class clazz) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("IncorrectClaimException with claim : "+value); + } + + @Override + protected boolean matchesSafely(IncorrectClaimException item) { + return item.getClaimValue().as(clazz).equals(value); + } + }; + } + + public static Matcher hasClaimValueArray(final Object value, final Class clazz) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("IncorrectClaimException with claim : "+value); + } + + @Override + protected boolean matchesSafely(IncorrectClaimException item) { + return Arrays.equals((Object[]) item.getClaimValue().as(clazz), (Object[])value); + } + }; + } + + public static Matcher hasNullClaim() { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("IncorrectClaimException with claim as null"); + } + + @Override + protected boolean matchesSafely(IncorrectClaimException item) { + boolean a = item.getClaimValue().isNull(); + String b = item.getClaimValue().toString(); + return item.getClaimValue().isNull(); + } + }; + } +} From dd22f32d5cfcffd6066a5ce949bcc58481cc2f5a Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Wed, 30 Mar 2022 03:35:54 -0500 Subject: [PATCH 178/293] Security: Bump `jackson-databind` to 2.13.2.2 (#566) * Security: Bump `jackson-databind` to 2.13.2.1 This PR bumps the `jackson-databind` dependency to 2.13.2.1 to address [CVE-2020-36518](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-36518) in that library * Bump to 2.13.2.2 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 55f531b8..fbc88df8 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -47,7 +47,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' testImplementation 'junit:junit:4.12' testImplementation 'net.jodah:concurrentunit:0.4.3' From 9793478af856483af7b1e75cd8d66096df77b18f Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 30 Mar 2022 14:09:19 +0530 Subject: [PATCH 179/293] Release 3.19.1 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84686b4b..a022328d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.19.1](https://github.com/auth0/java-jwt/tree/3.19.1) (2022-03-30) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.0...3.19.1) + +**Security** +- Security: Bump `jackson-databind` to 2.13.2.2 [\#566](https://github.com/auth0/java-jwt/pull/566) ([evansims](https://github.com/evansims)) + ## [3.19.0](https://github.com/auth0/java-jwt/tree/3.19.0) (2022-03-14) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.18.3...3.19.0) diff --git a/README.md b/README.md index a26170d2..260f6d6e 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.19.0 + 3.19.1 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.19.0' +implementation 'com.auth0:java-jwt:3.19.1' ``` ## Available Algorithms From ed58ef33d1ed800b84678acee2cab172684198d1 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 1 Apr 2022 15:35:37 +0530 Subject: [PATCH 180/293] Improved README structure --- README.md | 193 +++++++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 0f847cca..fb489d9b 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,29 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o > This library requires Java 8 or higher. The last version that supported Java 7 was 3.11.0. +## Table of Contents +- [**Installation**](#installation) +- [**Available Algorithms**](#available-algorithms) +- [**Quickstart**](#quickstart) + + [**Create and Sign a Token**](#create-and-sign-a-token) + + [**Verify a Token**](#verify-a-token) + + [**Decode a Token**](#decode-a-token) +- [**Usage**](#usage) + + [**Pick the algorithm**](#pick-the-algorithm) + + [**Time Validation**](#time-validation) + + [**Header Claims**](#header-claims) + + [**Payload Claims**](#payload-claims) + + [**Claim Class**](#claim-class) +- [**Javadoc**](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) + ## Installation -The library is available on both Maven Central and Bintray, and the Javadoc is published [here](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html). - +### Gradle + +```gradle +implementation 'com.auth0:java-jwt:3.19.0' +``` + ### Maven ```xml @@ -27,12 +46,6 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p ``` -### Gradle - -```gradle -implementation 'com.auth0:java-jwt:3.19.0' -``` - ## Available Algorithms The library implements JWT Verification and Signing using the following algorithms: @@ -49,73 +62,9 @@ The library implements JWT Verification and Signing using the following algorith | ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | | ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | -⚠️ Note - ECDSA with curve secp256k1 and SHA-256 will not be supported for Java 15+ by this library since it has been (disabled in Java 15)[https://www.oracle.com/java/technologies/javase/15-relnote-issues.html#JDK-8237219] - -## Usage - -### Pick the Algorithm - -The Algorithm defines how a token is signed and verified. It can be instantiated with the raw value of the secret in the case of HMAC algorithms, or the key pairs or `KeyProvider` in the case of RSA and ECDSA algorithms. Once created, the instance is reusable for token signing and verification operations. - -When using RSA or ECDSA algorithms and you just need to **sign** JWTs you can avoid specifying a Public Key by passing a `null` value. The same can be done with the Private Key when you just need to **verify** JWTs. - - -#### Using static secrets or keys: - -```java -//HMAC -Algorithm algorithmHS = Algorithm.HMAC256("secret"); - -//RSA -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance -Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); -``` - -> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). - -##### HMAC Key Length and Security - -When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recomendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. - -#### Using a KeyProvider: - -By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: - -- `getPublicKeyById(String kid)`: Its called during token signature verification and it should return the key used to verify the token. If key rotation is being used, e.g. [JWK](https://tools.ietf.org/html/rfc7517) it can fetch the correct rotation key using the id. (Or just return the same key all the time). -- `getPrivateKey()`: Its called during token signing and it should return the key that will be used to sign the JWT. -- `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. - - -The following example shows how this would work with `JwkStore`, an imaginary [JWK Set](https://auth0.com/docs/jwks) implementation. For simple key rotation using JWKS, try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. - -```java -final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}"); -final RSAPrivateKey privateKey = //Get the key instance -final String privateKeyId = //Create an Id for the above key +> Note - Support for ECDSA with curve secp256k1 and SHA-256 (ES256K) has been dropped since it has been [disabled in Java 15](https://www.oracle.com/java/technologies/javase/15-relnote-issues.html#JDK-8237219) -RSAKeyProvider keyProvider = new RSAKeyProvider() { - @Override - public RSAPublicKey getPublicKeyById(String kid) { - //Received 'kid' value might be null if it wasn't defined in the Token's header - RSAPublicKey publicKey = jwkStore.get(kid); - return (RSAPublicKey) publicKey; - } - - @Override - public RSAPrivateKey getPrivateKey() { - return privateKey; - } - - @Override - public String getPrivateKeyId() { - return privateKeyId; - } -}; - -Algorithm algorithm = Algorithm.RSA256(keyProvider); -//Use the Algorithm to create and verify JWTs. -``` +## Quickstart ### Create and Sign a Token @@ -151,7 +100,6 @@ You'll first need to create a `JWTCreator` instance by calling `JWT.create()`. U If a Claim couldn't be converted to JSON or the Key used in the signing process was invalid a `JWTCreationException` will raise. - ### Verify a Token You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` and passing the `Algorithm` instance. If you require the token to have specific Claim values, use the builder to define them. The instance returned by the method `build()` is reusable, so you can define it once and use it to verify different tokens. Finally call `verifier.verify()` passing the token. @@ -190,8 +138,87 @@ You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise. +### Decode a Token -#### Time Validation +```java +String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; +try { + DecodedJWT jwt = JWT.decode(token); +} catch (JWTDecodeException exception){ + //Invalid token +} +``` + +If the token has an invalid syntax or the header or payload are not JSONs, a `JWTDecodeException` will raise. + +## Usage + +### Pick the Algorithm + +The Algorithm defines how a token is signed and verified. It can be instantiated with the raw value of the secret in the case of HMAC algorithms, or the key pairs or `KeyProvider` in the case of RSA and ECDSA algorithms. Once created, the instance is reusable for token signing and verification operations. + +When using RSA or ECDSA algorithms and you just need to **sign** JWTs you can avoid specifying a Public Key by passing a `null` value. The same can be done with the Private Key when you just need to **verify** JWTs. + + +#### Using static secrets or keys: + +```java +//HMAC +Algorithm algorithmHS = Algorithm.HMAC256("secret"); + +//RSA +RSAPublicKey publicKey = //Get the key instance +RSAPrivateKey privateKey = //Get the key instance +Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); +``` + +> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). + +##### HMAC Key Length and Security + +When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recomendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. + +#### Using a KeyProvider: +//todo need to be updated +By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: + +- `getPublicKeyById(String kid)`: Its called during token signature verification and it should return the key used to verify the token. If key rotation is being used, e.g. [JWK](https://tools.ietf.org/html/rfc7517) it can fetch the correct rotation key using the id. (Or just return the same key all the time). +- `getPrivateKey()`: Its called during token signing and it should return the key that will be used to sign the JWT. +- `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. + + +The following example shows how this would work with `JwkStore`, an imaginary [JWK Set](https://auth0.com/docs/jwks) implementation. For simple key rotation using JWKS, try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. + +```java +final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}"); +final RSAPrivateKey privateKey = //Get the key instance +final String privateKeyId = //Create an Id for the above key + +RSAKeyProvider keyProvider = new RSAKeyProvider() { + @Override + public RSAPublicKey getPublicKeyById(String kid) { + //Received 'kid' value might be null if it wasn't defined in the Token's header + RSAPublicKey publicKey = jwkStore.get(kid); + return (RSAPublicKey) publicKey; + } + + @Override + public RSAPrivateKey getPrivateKey() { + return privateKey; + } + + @Override + public String getPrivateKeyId() { + return privateKeyId; + } +}; + +Algorithm algorithm = Algorithm.RSA256(keyProvider); +//Use the Algorithm to create and verify JWTs. +``` + + +### Time Validation The JWT token may include DateNumber fields that can be used to validate that: * The token was issued in a past date `"iat" < TODAY` @@ -227,20 +254,6 @@ Clock clock = new CustomClock(); //Must implement Clock interface JWTVerifier verifier = verification.build(clock); ``` -### Decode a Token - -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -try { - DecodedJWT jwt = JWT.decode(token); -} catch (JWTDecodeException exception){ - //Invalid token -} -``` - -If the token has an invalid syntax or the header or payload are not JSONs, a `JWTDecodeException` will raise. - - ### Header Claims #### Algorithm ("alg") From c1942944b3b906d749128279808ad86cd24b8b9f Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 12 Apr 2022 18:55:31 +0530 Subject: [PATCH 181/293] Add README instructions for new APIs --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb489d9b..065899f1 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,6 @@ Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recomendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. #### Using a KeyProvider: -//todo need to be updated By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: - `getPublicKeyById(String kid)`: Its called during token signature verification and it should return the key used to verify the token. If key rotation is being used, e.g. [JWK](https://tools.ietf.org/html/rfc7517) it can fetch the correct rotation key using the id. (Or just return the same key all the time). @@ -388,6 +387,7 @@ When creating a Token with the `JWT.create()` you can specify a custom Claim by String token = JWT.create() .withClaim("name", 123) .withArrayClaim("array", new Integer[]{1, 2, 3}) + .withNullClaim("claim_name") .sign(algorithm); ``` @@ -407,6 +407,9 @@ You can also verify custom Claims on the `JWT.require()` by calling `withClaim() JWTVerifier verifier = JWT.require(algorithm) .withClaim("name", 123) .withArrayClaim("array", 1, 2, 3) + .withNullClaim("null_value") //checks if the claim name provided has null value + .withClaimPresence("claim_presence") //checks if the claim name provided is in the payload + .withClaim("predicate", (claim, decodedJWT) -> "custom_check".equals(claim.asString())) //can be used to run custom verification .build(); DecodedJWT jwt = verifier.verify("my.jwt.token"); ``` @@ -423,7 +426,10 @@ The Claim class is a wrapper for the Claim values. It allows you to get the Clai * **asDouble()**: Returns the Double value or null if it can't be converted. * **asLong()**: Returns the Long value or null if it can't be converted. * **asString()**: Returns the String value or null if it can't be converted. -* **asDate()**: Returns the Date value or null if it can't be converted. This must be a NumericDate (Unix Epoch/Timestamp). Note that the [JWT Standard](https://tools.ietf.org/html/rfc7519#section-2) specified that all the *NumericDate* values must be in seconds. +* **asInstant()**: Returns the Instant value or null if it can't be converted. +* **asDate()**: Returns the Date value or null if it can't be converted. + +> For `asInstant()` and `asDate()` the value must be a NumericDate (Unix Epoch/Timestamp). Note that the [JWT Standard](https://tools.ietf.org/html/rfc7519#section-2) specified that all the *NumericDate* values must be in seconds. #### Custom Classes and Collections To obtain a Claim as a Collection you'll need to provide the **Class Type** of the contents to convert from. From 0f8a9bf20f215f9cdb78c50d7edbfe9d811cfdbc Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 12 Apr 2022 19:18:46 +0530 Subject: [PATCH 182/293] Replace expected exception with assertThrows (#572) * Replace expected exception with assertThrows --- .../java/com/auth0/jwt/JWTVerifierTest.java | 550 +++++++++--------- .../auth0/jwt/matchers/CustomMatchers.java | 114 ---- 2 files changed, 275 insertions(+), 389 deletions(-) delete mode 100644 lib/src/test/java/com/auth0/jwt/matchers/CustomMatchers.java diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 1cf79c8f..9dcb2cb1 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -19,9 +19,9 @@ import java.util.Date; import java.util.function.BiPredicate; -import static com.auth0.jwt.matchers.CustomMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; public class JWTVerifierTest { @@ -35,17 +35,18 @@ public class JWTVerifierTest { @Test public void shouldThrowWhenInitializedWithoutAlgorithm() { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("The Algorithm cannot be null"); - JWTVerifier.init(null); + IllegalArgumentException e = assertThrows(null, IllegalArgumentException.class, () -> + JWTVerifier.init(null)); + assertThat(e.getMessage(), is("The Algorithm cannot be null.")); } @Test public void shouldThrowWhenAlgorithmDoesntMatchTheTokensAlgorithm() { - exception.expect(AlgorithmMismatchException.class); - exception.expectMessage("The provided Algorithm doesn't match the one defined in the JWT's Header."); - JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC512("secret")).build(); - verifier.verify("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"); + AlgorithmMismatchException e = assertThrows(null, AlgorithmMismatchException.class, () -> { + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC512("secret")).build(); + verifier.verify("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"); + }); + assertThat(e.getMessage(), is("The provided Algorithm doesn't match the one defined in the JWT's Header.")); } @Test @@ -73,45 +74,45 @@ public void shouldValidateMultipleIssuers() { @Test public void shouldThrowOnInvalidIssuer() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'iss' value doesn't match the required issuer."); - exception.expect(hasClaimName(PublicClaims.ISSUER)); - exception.expect(hasClaimValue("auth0", String.class)); - - String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withIssuer("invalid") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("invalid") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); + assertThat(e.getClaimName(), is(PublicClaims.ISSUER)); + assertThat(e.getClaimValue().asString(), is("auth0")); } @Test public void shouldThrowOnNullIssuer() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'iss' value doesn't match the required issuer."); - exception.expect(hasClaimName(PublicClaims.ISSUER)); - exception.expect(hasNullClaim()); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOm51bGx9.OoiCLipSfflWxkFX2rytvtwEiJ8eAL0opkdXY_ap0qA"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withIssuer("auth0") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOm51bGx9.OoiCLipSfflWxkFX2rytvtwEiJ8eAL0opkdXY_ap0qA"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("auth0") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); + assertThat(e.getClaimName(), is(PublicClaims.ISSUER)); + assertThat(e.getClaimValue().isNull(), is(true)); } @Test public void shouldThrowOnMissingIssuer() { - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'iss' is not present in the JWT."); - exception.expect(hasMissingClaimName("iss")); - - String jwt = JWTCreator.init() - .sign(Algorithm.HMAC256("secret")); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String jwt = JWTCreator.init() + .sign(Algorithm.HMAC256("secret")); - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withIssuer("nope") - .build() - .verify(jwt); + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("nope") + .build() + .verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'iss' is not present in the JWT.")); + assertThat(e.getClaimName(), is("iss")); } @Test @@ -127,16 +128,16 @@ public void shouldValidateSubject() { @Test public void shouldThrowOnInvalidSubject() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'sub' value doesn't match the required one."); - exception.expect(hasClaimName(PublicClaims.SUBJECT)); - exception.expect(hasClaimValue("1234567890", String.class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withSubject("invalid") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withSubject("invalid") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'sub' value doesn't match the required one.")); + assertThat(e.getClaimName(), is(PublicClaims.SUBJECT)); + assertThat(e.getClaimValue().asString(), is("1234567890")); } @Test @@ -230,76 +231,75 @@ public void shouldAcceptAudienceWhenAnyOfAudienceAndAllContained() { @Test public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); - exception.expect(hasClaimName(PublicClaims.AUDIENCE)); - exception.expect(hasClaimValueArray(new String[] {"Mark","David","John"}, String[].class)); - - // Token 'aud' = ["Mark", "David", "John"] - String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAnyOfAudience("Joe", "Jim") - .build() - .verify(tokenArr); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud' = ["Mark", "David", "John"] + String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Joe", "Jim") + .build() + .verify(tokenArr); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @Test public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); - exception.expect(hasClaimName(PublicClaims.AUDIENCE)); - exception.expect(hasClaimValueArray(new String[] {"Mark","David","John"}, String[].class)); - - // Token 'aud' = ["Mark", "David", "John"] - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("Mark", "Joe") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience("Mark", "Joe") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @Test public void shouldThrowWhenAudienceClaimIsNull() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); - exception.expect(hasClaimName(PublicClaims.AUDIENCE)); - exception.expect(hasNullClaim()); - - // Token 'aud': null - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpudWxsfQ.bpPyquk3b8KepErKgTidjJ1ZwiOGuoTxam2_x7cElKI"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("nope") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud': null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpudWxsfQ.bpPyquk3b8KepErKgTidjJ1ZwiOGuoTxam2_x7cElKI"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience("nope") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimValue().isNull(), is(true)); } @Test public void shouldThrowWhenAudienceClaimIsMissing(){ - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'aud' is not present in the JWT."); - exception.expect(hasMissingClaimName("aud")); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; - - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("nope") - .build() - .verify(token); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience("nope") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' is not present in the JWT.")); + assertThat(e.getClaimName(), is("aud")); } @Test public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); - exception.expect(hasClaimName(PublicClaims.AUDIENCE)); - exception.expect(hasClaimValueArray(new String[] {null}, String[].class)); - - // Token 'aud': [null] - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpbbnVsbF19.2cBf7FbkX52h8Vmjnl1DY1PYe_J_YP0KsyeoeYmuca8"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAnyOfAudience("nope") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud': [null] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpbbnVsbF19.2cBf7FbkX52h8Vmjnl1DY1PYe_J_YP0KsyeoeYmuca8"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("nope") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {null})); } @Test @@ -384,113 +384,113 @@ public void shouldThrowOnNullCustomClaimName() { @Test public void shouldThrowWhenExpectedArrayClaimIsMissing() { - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'missing' is not present in the JWT."); - exception.expect(hasMissingClaimName("missing")); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcnJheSI6WzEsMiwzXX0.wKNFBcMdwIpdF9rXRxvexrzSM6umgSFqRO1WZj992YM"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withArrayClaim("missing", 1, 2, 3) - .build() - .verify(token); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcnJheSI6WzEsMiwzXX0.wKNFBcMdwIpdF9rXRxvexrzSM6umgSFqRO1WZj992YM"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("missing", 1, 2, 3) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'missing' is not present in the JWT.")); + assertThat(e.getClaimName(), is("missing")); } @Test public void shouldThrowWhenExpectedClaimIsMissing() { - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'missing' is not present in the JWT."); - exception.expect(hasMissingClaimName("missing")); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbSI6InRleHQifQ.aZ27Ze35VvTqxpaSIK5ZcnYHr4SrvANlUbDR8fw9qsQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("missing", "text") - .build() - .verify(token); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbSI6InRleHQifQ.aZ27Ze35VvTqxpaSIK5ZcnYHr4SrvANlUbDR8fw9qsQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("missing", "text") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'missing' is not present in the JWT.")); + assertThat(e.getClaimName(), is("missing")); } @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeString() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", "value") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", "value") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeInteger() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 123) - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", 123) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeDouble() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 23.45) - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", 23.45) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeBoolean() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", true) - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", true) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test public void shouldThrowOnInvalidCustomClaimValueOfTypeDate() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", new Date()) - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", new Date()) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test public void shouldThrowOnInvalidCustomClaimValue() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - exception.expect(hasClaimName("name")); - exception.expect(hasClaimValueArray(new String[] {"something"}, String[].class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", "check") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", "check") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); } @Test @@ -734,15 +734,15 @@ public void shouldValidateExpiresAtIfPresent() { @Test public void shouldThrowOnInvalidExpiresAtIfPresent() { - exception.expect(TokenExpiredException.class); - exception.expectMessage(startsWith("The Token has expired on")); - exception.expect(hasTokenExpiredOn(Instant.ofEpochSecond(1477592L))); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(mockOneSecondLater) - .verify(token); + TokenExpiredException e = assertThrows(null, TokenExpiredException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification + .build(mockOneSecondLater) + .verify(token); + }); + assertThat(e.getMessage(), is("The Token has expired on 1970-01-18T02:26:32Z.")); + assertThat(e.getExpiredOn(), is(Instant.ofEpochSecond(1477592L))); } @Test @@ -769,16 +769,16 @@ public void shouldValidateNotBeforeWithLeeway() { @Test public void shouldThrowOnInvalidNotBeforeIfPresent() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Token can't be used before 1970-01-18T02:26:32Z."); - exception.expect(hasClaimName(PublicClaims.NOT_BEFORE)); - exception.expect(hasClaimValue(1477592L, Long.class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0Nzc1OTJ9.wq4ZmnSF2VOxcQBxPLfeh1J2Ozy1Tj5iUaERm3FKaw8"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(mockOneSecondEarlier) - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0Nzc1OTJ9.wq4ZmnSF2VOxcQBxPLfeh1J2Ozy1Tj5iUaERm3FKaw8"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification + .build(mockOneSecondEarlier) + .verify(token); + }); + assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); + assertThat(e.getClaimName(), is(PublicClaims.NOT_BEFORE)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); } @Test @@ -804,16 +804,16 @@ public void shouldThrowOnNegativeNotBeforeLeeway() { // Issued At with future date @Test public void shouldThrowOnFutureIssuedAt() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Token can't be used before 1970-01-18T02:26:32Z."); - exception.expect(hasClaimName(PublicClaims.ISSUED_AT)); - exception.expect(hasClaimValue(1477592L, Long.class)); - - String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.CWq-6pUXl1bFg81vqOUZbZrheO2kUBd2Xr3FUZmvudE"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - DecodedJWT jwt = verification.build(mockOneSecondEarlier).verify(token); - assertThat(jwt, is(notNullValue())); + DecodedJWT jwt = verification.build(mockOneSecondEarlier).verify(token); + assertThat(jwt, is(notNullValue())); + }); + assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); + assertThat(e.getClaimName(), is(PublicClaims.ISSUED_AT)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); } // Issued At with future date and ignore flag @@ -829,16 +829,16 @@ public void shouldSkipIssuedAtVerificationWhenFlagIsPassed() { @Test public void shouldThrowOnInvalidIssuedAtIfPresent() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Token can't be used before 1970-01-18T02:26:32Z."); - exception.expect(hasClaimName(PublicClaims.ISSUED_AT)); - exception.expect(hasClaimValue(1477592L, Long.class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(mockOneSecondEarlier) - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification + .build(mockOneSecondEarlier) + .verify(token); + }); + assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); + assertThat(e.getClaimName(), is(PublicClaims.ISSUED_AT)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); } @Test @@ -887,16 +887,16 @@ public void shouldValidateJWTId() { @Test public void shouldThrowOnInvalidJWTId() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'jti' value doesn't match the required one."); - exception.expect(hasClaimName("jti")); - exception.expect(hasClaimValue("jwt_id_123", String.class)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJqd3RfaWRfMTIzIn0.0kegfXUvwOYioP8PDaLMY1IlV8HOAzSVz3EGL7-jWF4"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withJWTId("invalid") - .build() - .verify(token); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJqd3RfaWRfMTIzIn0.0kegfXUvwOYioP8PDaLMY1IlV8HOAzSVz3EGL7-jWF4"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withJWTId("invalid") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'jti' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("jti")); + assertThat(e.getClaimValue().asString(), is("jwt_id_123")); } @Test @@ -963,19 +963,19 @@ public void shouldSkipClaimValidationsIfNoClaimsRequired() { @Test public void shouldThrowWhenVerifyingClaimPresenceButClaimNotPresent() { - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'missing' is not present in the JWT."); - exception.expect(hasMissingClaimName("missing")); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("custom", "") + .sign(Algorithm.HMAC256("secret")); - String jwt = JWTCreator.init() - .withClaim("custom", "") - .sign(Algorithm.HMAC256("secret")); - - JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaimPresence("missing") - .build(); + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("missing") + .build(); - verifier.verify(jwt); + verifier.verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'missing' is not present in the JWT.")); + assertThat(e.getClaimName(), is("missing")); } @Test @@ -1120,19 +1120,19 @@ public void shouldSuccessfullyVerifyClaimWithPredicate() { @Test public void shouldThrowWhenPredicateReturnsFalse() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'claimName' value doesn't match the required one."); - exception.expect(hasClaimName("claimName")); - exception.expect(hasClaimValue("claimValue", String.class)); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("claimName", "claimValue") + .sign(Algorithm.HMAC256("secret")); - String jwt = JWTCreator.init() - .withClaim("claimName", "claimValue") - .sign(Algorithm.HMAC256("secret")); - - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("claimName", (claim, decodedJWT) -> "nope".equals(claim.asString())) - .build() - .verify(jwt); + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("claimName", (claim, decodedJWT) -> "nope".equals(claim.asString())) + .build() + .verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'claimName' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("claimName")); + assertThat(e.getClaimValue().asString(), is("claimValue")); } @Test @@ -1162,34 +1162,34 @@ public void shouldSuccessfullyVerifyClaimWithNull() { @Test public void shouldThrowWhenNullClaimHasValue() { - exception.expect(IncorrectClaimException.class); - exception.expectMessage("The Claim 'claimName' value doesn't match the required one."); - exception.expect(hasClaimName("claimName")); - exception.expect(hasClaimValue("value", String.class)); + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("claimName", "value") + .sign(Algorithm.HMAC256("secret")); - String jwt = JWTCreator.init() - .withClaim("claimName", "value") - .sign(Algorithm.HMAC256("secret")); - - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withNullClaim("claimName") - .build() - .verify(jwt); + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withNullClaim("claimName") + .build() + .verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'claimName' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("claimName")); + assertThat(e.getClaimValue().asString(), is("value")); } @Test public void shouldThrowWhenNullClaimIsMissing() { - exception.expect(MissingClaimException.class); - exception.expectMessage("The Claim 'anotherClaimName' is not present in the JWT."); - exception.expect(hasMissingClaimName("anotherClaimName")); - - String jwt = JWTCreator.init() - .withClaim("claimName", "value") - .sign(Algorithm.HMAC256("secret")); - - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withNullClaim("anotherClaimName") - .build() - .verify(jwt); + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("claimName", "value") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withNullClaim("anotherClaimName") + .build() + .verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'anotherClaimName' is not present in the JWT.")); + assertThat(e.getClaimName(), is("anotherClaimName")); } } diff --git a/lib/src/test/java/com/auth0/jwt/matchers/CustomMatchers.java b/lib/src/test/java/com/auth0/jwt/matchers/CustomMatchers.java deleted file mode 100644 index acff9b0f..00000000 --- a/lib/src/test/java/com/auth0/jwt/matchers/CustomMatchers.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.auth0.jwt.matchers; - -import com.auth0.jwt.exceptions.IncorrectClaimException; -import com.auth0.jwt.exceptions.MissingClaimException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.auth0.jwt.interfaces.Claim; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; - -import java.time.Instant; -import java.util.Arrays; - -public class CustomMatchers { - public static Matcher hasMissingClaimName(final String claimName) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("MissingClaimException with claim name: "+claimName); - } - - @Override - protected boolean matchesSafely(MissingClaimException item) { - return item.getClaimName().equals(claimName); - } - }; - } - - public static Matcher hasTokenExpiredOn(final Instant instant) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("TokenExpiredException with expired time as: "+instant.getEpochSecond()); - } - - @Override - protected boolean matchesSafely(TokenExpiredException item) { - return item.getExpiredOn().equals(instant); - } - }; - } - - public static Matcher hasClaimName(final String claimName) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("IncorrectClaimException with claim name: "+claimName); - } - - @Override - protected boolean matchesSafely(IncorrectClaimException item) { - return item.getClaimName().equals(claimName); - } - }; - } - - public static Matcher hasClaimValue(final Object value, final Class clazz) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("IncorrectClaimException with claim : "+value); - } - - @Override - protected boolean matchesSafely(IncorrectClaimException item) { - return item.getClaimValue().as(clazz).equals(value); - } - }; - } - - public static Matcher hasClaimInstant(final Instant value, final Class clazz) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("IncorrectClaimException with claim : "+value); - } - - @Override - protected boolean matchesSafely(IncorrectClaimException item) { - return item.getClaimValue().as(clazz).equals(value); - } - }; - } - - public static Matcher hasClaimValueArray(final Object value, final Class clazz) { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("IncorrectClaimException with claim : "+value); - } - - @Override - protected boolean matchesSafely(IncorrectClaimException item) { - return Arrays.equals((Object[]) item.getClaimValue().as(clazz), (Object[])value); - } - }; - } - - public static Matcher hasNullClaim() { - return new TypeSafeMatcher() { - @Override - public void describeTo(Description description) { - description.appendText("IncorrectClaimException with claim as null"); - } - - @Override - protected boolean matchesSafely(IncorrectClaimException item) { - boolean a = item.getClaimValue().isNull(); - String b = item.getClaimValue().toString(); - return item.getClaimValue().isNull(); - } - }; - } -} From af04b229327f863cdde7c41c77c076e39a6b3742 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 13 Apr 2022 15:05:02 +0530 Subject: [PATCH 183/293] [SDK-3231] Added support for multiple checks on a single claim (#573) * Added support for multiple checks on a single claim * Fix codecov CI failure * Allow pseudo comparison for codecov * Remove newly added parameters and check codecov disabled * Reenabled changes check in codecov * Trigger Build * Refactor ExpectedCheckHolder from interfaces to impl package * Refactored code to improve coverage report --- .../main/java/com/auth0/jwt/JWTVerifier.java | 153 ++++++----- .../auth0/jwt/impl/ExpectedCheckHolder.java | 25 ++ .../java/com/auth0/jwt/JWTVerifierTest.java | 241 ++++++++++++------ 3 files changed, 282 insertions(+), 137 deletions(-) create mode 100644 lib/src/main/java/com/auth0/jwt/impl/ExpectedCheckHolder.java diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index ba19feb9..801dac24 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -6,6 +6,7 @@ import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.impl.ExpectedCheckHolder; import com.auth0.jwt.interfaces.Verification; import java.time.Clock; @@ -25,12 +26,12 @@ */ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { private final Algorithm algorithm; - final Map> expectedChecks; + final List expectedChecks; private final JWTParser parser; - JWTVerifier(Algorithm algorithm, Map> expectedChecks) { + JWTVerifier(Algorithm algorithm, List expectedChecks) { this.algorithm = algorithm; - this.expectedChecks = Collections.unmodifiableMap(expectedChecks); + this.expectedChecks = Collections.unmodifiableList(expectedChecks); this.parser = new JWTParser(); } @@ -50,7 +51,7 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { */ public static class BaseVerification implements Verification { private final Algorithm algorithm; - private final Map> expectedChecks; + private final List expectedChecks; private long defaultLeeway; private final Map customLeeways; private boolean ignoreIssuedAt; @@ -62,7 +63,7 @@ public static class BaseVerification implements Verification { } this.algorithm = algorithm; - this.expectedChecks = new LinkedHashMap<>(); + this.expectedChecks = new ArrayList<>(); this.customLeeways = new HashMap<>(); this.defaultLeeway = 0; } @@ -70,7 +71,10 @@ public static class BaseVerification implements Verification { @Override public Verification withIssuer(String... issuer) { List value = isNullOrEmpty(issuer) ? null : Arrays.asList(issuer); - checkIfNeedToRemove(PublicClaims.ISSUER, value, ((claim, decodedJWT) -> { + addCheck(PublicClaims.ISSUER, ((claim, decodedJWT) -> { + if (verifyNull(claim, value)) { + return true; + } if (value == null || !value.contains(claim.asString())) { throw new IncorrectClaimException( "The Claim 'iss' value doesn't match the required issuer.", PublicClaims.ISSUER, claim); @@ -82,23 +86,40 @@ public Verification withIssuer(String... issuer) { @Override public Verification withSubject(String subject) { - checkIfNeedToRemove(PublicClaims.SUBJECT, subject, (claim, decodedJWT) -> subject.equals(claim.asString())); + addCheck(PublicClaims.SUBJECT, (claim, decodedJWT) -> + verifyNull(claim, subject) || subject.equals(claim.asString())); return this; } @Override public Verification withAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> - assertValidAudienceClaim(claim, decodedJWT.getAudience(), value, true))); + addCheck(PublicClaims.AUDIENCE, ((claim, decodedJWT) -> { + if (verifyNull(claim, value)) { + return true; + } + if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, true)) { + throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", + PublicClaims.AUDIENCE, claim); + } + return true; + })); return this; } @Override public Verification withAnyOfAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - checkIfNeedToRemove(PublicClaims.AUDIENCE, value, ((claim, decodedJWT) -> - assertValidAudienceClaim(claim, decodedJWT.getAudience(), value, false))); + addCheck(PublicClaims.AUDIENCE, ((claim, decodedJWT) -> { + if (verifyNull(claim, value)) { + return true; + } + if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, false)) { + throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", + PublicClaims.AUDIENCE, claim); + } + return true; + })); return this; } @@ -138,14 +159,16 @@ public Verification ignoreIssuedAt() { @Override public Verification withJWTId(String jwtId) { - checkIfNeedToRemove(PublicClaims.JWT_ID, jwtId, ((claim, decodedJWT) -> jwtId.equals(claim.asString()))); + addCheck(PublicClaims.JWT_ID, ((claim, decodedJWT) -> + verifyNull(claim, jwtId) || jwtId.equals(claim.asString()))); return this; } @Override public Verification withClaimPresence(String name) throws IllegalArgumentException { assertNonNull(name); - withClaim(name, ((claim, decodedJWT) -> assertClaimPresence(name, claim))); + //since addCheck already checks presence, we just return true + withClaim(name, ((claim, decodedJWT) -> true)); return this; } @@ -159,35 +182,40 @@ public Verification withNullClaim(String name) throws IllegalArgumentException { @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asBoolean()))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asBoolean()))); return this; } @Override public Verification withClaim(String name, Integer value) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asInt()))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asInt()))); return this; } @Override public Verification withClaim(String name, Long value) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asLong()))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asLong()))); return this; } @Override public Verification withClaim(String name, Double value) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asDouble()))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asDouble()))); return this; } @Override public Verification withClaim(String name, String value) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, value, ((claim, decodedJWT) -> value.equals(claim.asString()))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asString()))); return this; } @@ -201,8 +229,9 @@ public Verification withClaim(String name, Instant value) throws IllegalArgument assertNonNull(name); // Since date-time claims are serialized as epoch seconds, // we need to compare them with only seconds-granularity - checkIfNeedToRemove(name, value, - ((claim, decodedJWT) -> value.truncatedTo(ChronoUnit.SECONDS).equals(claim.asInstant()))); + addCheck(name, + ((claim, decodedJWT) -> verifyNull(claim, value) + || value.truncatedTo(ChronoUnit.SECONDS).equals(claim.asInstant()))); return this; } @@ -210,28 +239,32 @@ public Verification withClaim(String name, Instant value) throws IllegalArgument public Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, predicate, predicate); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, predicate) + || predicate.test(claim, decodedJWT))); return this; } @Override public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } @Override public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } @Override public Verification withArrayClaim(String name, Long... items) throws IllegalArgumentException { assertNonNull(name); - checkIfNeedToRemove(name, items, ((claim, decodedJWT) -> assertValidCollectionClaim(claim, items))); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } @@ -268,13 +301,13 @@ private void addMandatoryClaimChecks() { long notBeforeLeeway = getLeewayFor(PublicClaims.NOT_BEFORE); long issuedAtLeeway = getLeewayFor(PublicClaims.ISSUED_AT); - expectedChecks.put(PublicClaims.EXPIRES_AT, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.EXPIRES_AT, claim, expiresAtLeeway, true)); - expectedChecks.put(PublicClaims.NOT_BEFORE, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.NOT_BEFORE, claim, notBeforeLeeway, false)); + expectedChecks.add(constructExpectedCheck(PublicClaims.EXPIRES_AT, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.EXPIRES_AT, claim, expiresAtLeeway, true))); + expectedChecks.add(constructExpectedCheck(PublicClaims.NOT_BEFORE, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.NOT_BEFORE, claim, notBeforeLeeway, false))); if (!ignoreIssuedAt) { - expectedChecks.put(PublicClaims.ISSUED_AT, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.ISSUED_AT, claim, issuedAtLeeway, false)); + expectedChecks.add(constructExpectedCheck(PublicClaims.ISSUED_AT, (claim, decodedJWT) -> + assertValidInstantClaim(PublicClaims.ISSUED_AT, claim, issuedAtLeeway, false))); } } @@ -294,8 +327,7 @@ private boolean assertValidCollectionClaim(Claim claim, Object[] expectedClaimVa } } } else { - claimArr = claim.isNull() || claim.isMissing() - ? Collections.emptyList() : Arrays.asList(claim.as(Object[].class)); + claimArr = Arrays.asList(claim.as(Object[].class)); } List valueArr = Arrays.asList(expectedClaimValue); return claimArr.containsAll(valueArr); @@ -329,24 +361,12 @@ private boolean assertInstantIsPast(Instant claimVal, long leeway, Instant now) } private boolean assertValidAudienceClaim( - Claim claim, List audience, List values, boolean shouldContainAll ) { - if (audience == null || (shouldContainAll && !audience.containsAll(values)) - || (!shouldContainAll && Collections.disjoint(audience, values))) { - throw new IncorrectClaimException( - "The Claim 'aud' value doesn't contain the required audience.", PublicClaims.AUDIENCE, claim); - } - return true; - } - - private boolean assertClaimPresence(String name, Claim claim) { - if (claim.isMissing()) { - throw new MissingClaimException(name); - } - return true; + return !(audience == null || (shouldContainAll && !audience.containsAll(values)) + || (!shouldContainAll && Collections.disjoint(audience, values))); } private void assertPositive(long leeway) { @@ -361,13 +381,31 @@ private void assertNonNull(String name) { } } - private void checkIfNeedToRemove(String name, Object value, BiPredicate predicate) { - if (value == null) { - expectedChecks.remove(name); - return; - } - expectedChecks.put(name, (claim, decodedJWT) -> assertClaimPresence(name, claim) - && predicate.test(claim, decodedJWT)); + private void addCheck(String name, BiPredicate predicate) { + expectedChecks.add(constructExpectedCheck(name, (claim, decodedJWT) -> { + if (claim.isMissing()) { + throw new MissingClaimException(name); + } + return predicate.test(claim, decodedJWT); + })); + } + + private ExpectedCheckHolder constructExpectedCheck(String claimName, BiPredicate check) { + return new ExpectedCheckHolder() { + @Override + public String getClaimName() { + return claimName; + } + + @Override + public boolean verify(Claim claim, DecodedJWT decodedJWT) { + return check.test(claim, decodedJWT); + } + }; + } + + private boolean verifyNull(Claim claim, Object value) { + return value == null && claim.isNull(); } private boolean isNullOrEmpty(String[] args) { @@ -431,15 +469,14 @@ private void verifyAlgorithm(DecodedJWT jwt, Algorithm expectedAlgorithm) throws } } - private void verifyClaims(DecodedJWT jwt, Map> claims) + private void verifyClaims(DecodedJWT jwt, List expectedChecks) throws TokenExpiredException, InvalidClaimException { - for (Map.Entry> entry : claims.entrySet()) { + for (ExpectedCheckHolder expectedCheck : expectedChecks) { boolean isValid; - String claimName = entry.getKey(); - BiPredicate expectedCheck = entry.getValue(); + String claimName = expectedCheck.getClaimName(); Claim claim = jwt.getClaim(claimName); - isValid = expectedCheck.test(claim, jwt); + isValid = expectedCheck.verify(claim, jwt); if (!isValid) { throw new IncorrectClaimException( diff --git a/lib/src/main/java/com/auth0/jwt/impl/ExpectedCheckHolder.java b/lib/src/main/java/com/auth0/jwt/impl/ExpectedCheckHolder.java new file mode 100644 index 00000000..6737031c --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/ExpectedCheckHolder.java @@ -0,0 +1,25 @@ +package com.auth0.jwt.impl; + +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; + +/** + * This holds the checks that are run to verify a JWT. + */ +public interface ExpectedCheckHolder { + /** + * The claim name that will be checked. + * + * @return the claim name + */ + String getClaimName(); + + /** + * The verification that will be run. + * + * @param claim the claim for which verification is done + * @param decodedJWT the JWT on which verification is done + * @return whether the verification passed or not + */ + boolean verify(Claim claim, DecodedJWT decodedJWT); +} diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 9dcb2cb1..72e5656f 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -56,8 +56,18 @@ public void shouldValidateIssuer() { .withIssuer("auth0") .build() .verify(token); - assertThat(jwt, is(notNullValue())); + + // "iss": ["auth0", "okta"] + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, ()-> { + String token1 = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer((String[]) null) + .build() + .verify(token1); + }); + + assertThat(e.getClaimName(), is("iss")); } @Test @@ -179,7 +189,7 @@ public void shouldAllowWithAnyOfAudienceVerificationToOverrideWithAudience() { assertThat(exception, is(instanceOf(IncorrectClaimException.class))); assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - DecodedJWT jwt = verification.withAnyOfAudience("Mark", "Jim").build().verify(token); + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")).withAnyOfAudience("Mark", "Jim").build().verify(token); assertThat(jwt, is(notNullValue())); } @@ -201,7 +211,7 @@ public void shouldAllowWithAudienceVerificationToOverrideWithAnyOfAudience() { assertThat(exception, is(instanceOf(IncorrectClaimException.class))); assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - DecodedJWT jwt = verification.withAudience("Mark").build().verify(token); + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")).withAudience("Mark").build().verify(token); assertThat(jwt, is(notNullValue())); } @@ -303,75 +313,15 @@ public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { } @Test - public void shouldRemoveAudienceWhenPassingNullReference() { - Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) - .withAudience((String) null) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); - - verifier = JWTVerifier.init(algorithm) + public void shouldNotReplaceWhenMultipleChecksAreAdded() { + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience((String[]) null) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); - - verifier = JWTVerifier.init(algorithm) .withAudience() - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey(PublicClaims.AUDIENCE))); - - String emptyAud = " "; - verifier = JWTVerifier.init(algorithm) - .withAudience(emptyAud) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - } - - @Test - public void shouldRemoveAudienceWhenPassingNull() { - Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) - .withAudience("John") - .withAudience((String) null) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("aud"))); - - verifier = JWTVerifier.init(algorithm) - .withAudience("John") - .withAudience((String[]) null) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("aud"))); - } - - @Test - public void shouldRemoveAudienceWhenPassingNullWithAnyAudience() { - Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience("John") - .withAnyOfAudience((String) null) - .build(); - - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("aud"))); - - verifier = JWTVerifier.init(algorithm) - .withAnyOfAudience("John") .withAnyOfAudience((String[]) null) + .withAnyOfAudience() .build(); - assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("aud"))); + assertThat(verifier.expectedChecks.size(), is(7)); //3 extra mandatory checks exp, nbf, iat } @Test @@ -561,14 +511,14 @@ public void shouldValidateCustomClaimOfTypeDate() { } @Test - public void shouldRemoveCustomClaimOfTypeDateWhenNull() { + public void shouldNotRemoveCustomClaimOfTypeDateWhenNull() { JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", new Date()) .withClaim("name", (Date) null) .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(5)); } @Test @@ -900,7 +850,7 @@ public void shouldThrowOnInvalidJWTId() { } @Test - public void shouldRemoveClaimWhenPassingNull() { + public void shouldNotRemoveClaimWhenPassingNull() { Algorithm algorithm = mock(Algorithm.class); JWTVerifier verifier = JWTVerifier.init(algorithm) .withIssuer("iss") @@ -908,7 +858,7 @@ public void shouldRemoveClaimWhenPassingNull() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(5)); verifier = JWTVerifier.init(algorithm) .withIssuer("iss") @@ -916,32 +866,32 @@ public void shouldRemoveClaimWhenPassingNull() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(5)); } @Test - public void shouldRemoveIssuerWhenPassingNullReference() { + public void shouldNotRemoveIssuerWhenPassingNullReference() { Algorithm algorithm = mock(Algorithm.class); JWTVerifier verifier = JWTVerifier.init(algorithm) .withIssuer((String) null) .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(4)); verifier = JWTVerifier.init(algorithm) .withIssuer((String[]) null) .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(4)); verifier = JWTVerifier.init(algorithm) .withIssuer() .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("iss"))); + assertThat(verifier.expectedChecks.size(), is(4)); String emptyIss = " "; verifier = JWTVerifier.init(algorithm) @@ -1111,7 +1061,6 @@ public void shouldSuccessfullyVerifyClaimWithPredicate() { JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("claimName", (claim, decodedJWT) -> "claimValue".equals(claim.asString())) - .withClaim(PublicClaims.ISSUED_AT, ((claim, decodedJWT) -> false)) .build(); DecodedJWT decodedJWT = verifier.verify(jwt); @@ -1136,14 +1085,14 @@ public void shouldThrowWhenPredicateReturnsFalse() { } @Test - public void shouldRemovePredicateCheckForNull() { + public void shouldNotRemovePredicateCheckForNull() { JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("claimName", (claim, decodedJWT) -> "nope".equals(claim.asString())) .withClaim("claimName", (BiPredicate) null) .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verifier.expectedChecks, not(hasKey("claimName"))); + assertThat(verifier.expectedChecks.size(), is(5)); } @Test @@ -1192,4 +1141,138 @@ public void shouldThrowWhenNullClaimIsMissing() { assertThat(e.getMessage(), is("The Claim 'anotherClaimName' is not present in the JWT.")); assertThat(e.getClaimName(), is("anotherClaimName")); } + + @Test + public void shouldCheckForNullValuesForSubject() { + // sub = null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOm51bGx9.y5brmQQ05OYwVvlTg83njUrz6tfpdyWNh17LHU6DxmI"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withSubject(null) + .build() + .verify(token); + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldCheckForNullValuesInIssuer() { + // iss = null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOm51bGx9.OoiCLipSfflWxkFX2rytvtwEiJ8eAL0opkdXY_ap0qA"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer((String) null) + .withIssuer((String[]) null) + .withIssuer() + .build() + .verify(token); + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldCheckForNullValuesInJwtId() { + // jti = null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOm51bGx9.z_MDyl8uPGH0q0jeB54wbYt3bwKXamU_3MO8LofGvZs"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withJWTId(null) + .build() + .verify(token); + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldCheckForNullValuesInCustomClaims() { + // jti = null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b20iOm51bGx9.inAuN3Q9UZ6WgbB63O43B1ero2MTqnfzzumr_5qYIls"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("custom", (Boolean) null) + .withClaim("custom", (Integer) null) + .withClaim("custom", (Long) null) + .withClaim("custom", (Double) null) + .withClaim("custom", (String) null) + .withClaim("custom", (Date) null) + .withClaim("custom", (Instant) null) + .withClaim("custom", (BiPredicate) null) + .withArrayClaim("custom", (String[]) null) + .withArrayClaim("custom", (Integer[]) null) + .withArrayClaim("custom", (Long[]) null) + .build() + .verify(token); + assertThat(jwt, is(notNullValue())); + } + + + @Test + public void shouldCheckForNullValuesForAudience() { + // aud = null + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpudWxsfQ.bpPyquk3b8KepErKgTidjJ1ZwiOGuoTxam2_x7cElKI"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience((String[]) null) + .withAudience((String) null) + .withAudience() + .withAnyOfAudience((String[]) null) + .withAnyOfAudience((String) null) + .withAnyOfAudience() + .build() + .verify(token); + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldCheckForClaimPresenceEvenForNormalClaimChecks() { + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjpudWxsfQ.bpPyquk3b8KepErKgTidjJ1ZwiOGuoTxam2_x7cElKI"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("custom", true) + .build() + .verify(token); + }); + assertThat(e.getClaimName(), is("custom")); + } + + @Test + public void shouldCheckForWrongLongClaim() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b20iOjF9.00btiK0sv8pQ2T-hOr9GC5x2osi7--Bsk4pS5cTikqQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("custom", 2L) + .build() + .verify(token); + }); + assertThat(e.getClaimName(), is("custom")); + assertThat(e.getClaimValue().asLong(), is(1L)); + } + + @Test + public void shouldCheckForWrongLongArrayClaim() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b20iOlsxXX0.R9ZSmgtJng062rcEc59u4VKCq89Yk5VlkN9BuMTMvr0"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("custom", 2L) + .build() + .verify(token); + }); + assertThat(e.getClaimName(), is("custom")); + } + + @Test + public void shouldCheckForWrongStringArrayClaim() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b20iOlsxXX0.R9ZSmgtJng062rcEc59u4VKCq89Yk5VlkN9BuMTMvr0"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("custom", "2L") + .build() + .verify(token); + }); + assertThat(e.getClaimName(), is("custom")); + } + + @Test + public void shouldCheckForWrongIntegerArrayClaim() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b20iOlsxXX0.R9ZSmgtJng062rcEc59u4VKCq89Yk5VlkN9BuMTMvr0"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("custom", 2) + .build() + .verify(token); + }); + assertThat(e.getClaimName(), is("custom")); + } } From 05c925edc68eb072e6910055891d1317be2325a2 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 13 Apr 2022 09:58:21 -0500 Subject: [PATCH 184/293] [SDK-3226] Expose claim header constants --- lib/src/main/java/HeaderParams.java | 29 +++++++++++ .../main/java/com/auth0/jwt/JWTCreator.java | 28 +++++----- .../main/java/com/auth0/jwt/JWTVerifier.java | 43 ++++++++------- .../java/com/auth0/jwt/StandardClaims.java | 48 +++++++++++++++++ .../auth0/jwt/impl/HeaderDeserializer.java | 9 ++-- .../auth0/jwt/impl/PayloadDeserializer.java | 15 +++--- .../com/auth0/jwt/impl/PayloadSerializer.java | 3 +- .../java/com/auth0/jwt/impl/PublicClaims.java | 23 -------- .../java/com/auth0/jwt/JWTCreatorTest.java | 21 ++++---- .../java/com/auth0/jwt/JWTDecoderTest.java | 4 +- lib/src/test/java/com/auth0/jwt/JWTTest.java | 6 +-- .../java/com/auth0/jwt/JWTVerifierTest.java | 52 +++++++++---------- 12 files changed, 166 insertions(+), 115 deletions(-) create mode 100644 lib/src/main/java/HeaderParams.java create mode 100644 lib/src/main/java/com/auth0/jwt/StandardClaims.java delete mode 100644 lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java diff --git a/lib/src/main/java/HeaderParams.java b/lib/src/main/java/HeaderParams.java new file mode 100644 index 00000000..e15877a2 --- /dev/null +++ b/lib/src/main/java/HeaderParams.java @@ -0,0 +1,29 @@ +package com.auth0.jwt; + +/** + * Contains constants representing the JWT header parameter names. + */ +public final class HeaderParams { + + private HeaderParams() {} + + /** + * The algorithm used to sign a JWT. + */ + public static String ALGORITHM = "alg"; + + /** + * The content type of a JWT. + */ + public static String CONTENT_TYPE = "cty"; + + /** + * The media type of a JWT. + */ + public static String TYPE = "typ"; + + /** + * The key ID of a JWT used to specify the key for signature validation. + */ + public static String KEY_ID = "kid"; +} diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index e1159ae4..f0299d22 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -104,7 +104,7 @@ public Builder withHeader(Map headerClaims) { * @return this same Builder instance. */ public Builder withKeyId(String keyId) { - this.headerClaims.put(PublicClaims.KEY_ID, keyId); + this.headerClaims.put(HeaderParams.KEY_ID, keyId); return this; } @@ -115,7 +115,7 @@ public Builder withKeyId(String keyId) { * @return this same Builder instance. */ public Builder withIssuer(String issuer) { - addClaim(PublicClaims.ISSUER, issuer); + addClaim(StandardClaims.ISSUER, issuer); return this; } @@ -126,7 +126,7 @@ public Builder withIssuer(String issuer) { * @return this same Builder instance. */ public Builder withSubject(String subject) { - addClaim(PublicClaims.SUBJECT, subject); + addClaim(StandardClaims.SUBJECT, subject); return this; } @@ -137,7 +137,7 @@ public Builder withSubject(String subject) { * @return this same Builder instance. */ public Builder withAudience(String... audience) { - addClaim(PublicClaims.AUDIENCE, audience); + addClaim(StandardClaims.AUDIENCE, audience); return this; } @@ -149,7 +149,7 @@ public Builder withAudience(String... audience) { * @return this same Builder instance. */ public Builder withExpiresAt(Date expiresAt) { - addClaim(PublicClaims.EXPIRES_AT, expiresAt); + addClaim(StandardClaims.EXPIRES_AT, expiresAt); return this; } @@ -161,7 +161,7 @@ public Builder withExpiresAt(Date expiresAt) { * @return this same Builder instance. */ public Builder withExpiresAt(Instant expiresAt) { - addClaim(PublicClaims.EXPIRES_AT, expiresAt); + addClaim(StandardClaims.EXPIRES_AT, expiresAt); return this; } @@ -173,7 +173,7 @@ public Builder withExpiresAt(Instant expiresAt) { * @return this same Builder instance. */ public Builder withNotBefore(Date notBefore) { - addClaim(PublicClaims.NOT_BEFORE, notBefore); + addClaim(StandardClaims.NOT_BEFORE, notBefore); return this; } @@ -185,7 +185,7 @@ public Builder withNotBefore(Date notBefore) { * @return this same Builder instance. */ public Builder withNotBefore(Instant notBefore) { - addClaim(PublicClaims.NOT_BEFORE, notBefore); + addClaim(StandardClaims.NOT_BEFORE, notBefore); return this; } @@ -197,7 +197,7 @@ public Builder withNotBefore(Instant notBefore) { * @return this same Builder instance. */ public Builder withIssuedAt(Date issuedAt) { - addClaim(PublicClaims.ISSUED_AT, issuedAt); + addClaim(StandardClaims.ISSUED_AT, issuedAt); return this; } @@ -209,7 +209,7 @@ public Builder withIssuedAt(Date issuedAt) { * @return this same Builder instance. */ public Builder withIssuedAt(Instant issuedAt) { - addClaim(PublicClaims.ISSUED_AT, issuedAt); + addClaim(StandardClaims.ISSUED_AT, issuedAt); return this; } @@ -220,7 +220,7 @@ public Builder withIssuedAt(Instant issuedAt) { * @return this same Builder instance. */ public Builder withJWTId(String jwtId) { - addClaim(PublicClaims.JWT_ID, jwtId); + addClaim(StandardClaims.JWT_ID, jwtId); return this; } @@ -543,9 +543,9 @@ public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCrea if (algorithm == null) { throw new IllegalArgumentException("The Algorithm cannot be null."); } - headerClaims.put(PublicClaims.ALGORITHM, algorithm.getName()); - if (!headerClaims.containsKey(PublicClaims.TYPE)) { - headerClaims.put(PublicClaims.TYPE, "JWT"); + headerClaims.put(HeaderParams.ALGORITHM, algorithm.getName()); + if (!headerClaims.containsKey(HeaderParams.TYPE)) { + headerClaims.put(HeaderParams.TYPE, "JWT"); } String signingKeyId = algorithm.getSigningKeyId(); if (signingKeyId != null) { diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 801dac24..223de7f5 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -3,7 +3,6 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; import com.auth0.jwt.impl.JWTParser; -import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.impl.ExpectedCheckHolder; @@ -71,13 +70,13 @@ public static class BaseVerification implements Verification { @Override public Verification withIssuer(String... issuer) { List value = isNullOrEmpty(issuer) ? null : Arrays.asList(issuer); - addCheck(PublicClaims.ISSUER, ((claim, decodedJWT) -> { + addCheck(StandardClaims.ISSUER, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (value == null || !value.contains(claim.asString())) { throw new IncorrectClaimException( - "The Claim 'iss' value doesn't match the required issuer.", PublicClaims.ISSUER, claim); + "The Claim 'iss' value doesn't match the required issuer.", StandardClaims.ISSUER, claim); } return true; })); @@ -86,7 +85,7 @@ public Verification withIssuer(String... issuer) { @Override public Verification withSubject(String subject) { - addCheck(PublicClaims.SUBJECT, (claim, decodedJWT) -> + addCheck(StandardClaims.SUBJECT, (claim, decodedJWT) -> verifyNull(claim, subject) || subject.equals(claim.asString())); return this; } @@ -94,13 +93,13 @@ public Verification withSubject(String subject) { @Override public Verification withAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - addCheck(PublicClaims.AUDIENCE, ((claim, decodedJWT) -> { + addCheck(StandardClaims.AUDIENCE, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, true)) { throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", - PublicClaims.AUDIENCE, claim); + StandardClaims.AUDIENCE, claim); } return true; })); @@ -110,13 +109,13 @@ public Verification withAudience(String... audience) { @Override public Verification withAnyOfAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - addCheck(PublicClaims.AUDIENCE, ((claim, decodedJWT) -> { + addCheck(StandardClaims.AUDIENCE, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, false)) { throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", - PublicClaims.AUDIENCE, claim); + StandardClaims.AUDIENCE, claim); } return true; })); @@ -133,21 +132,21 @@ public Verification acceptLeeway(long leeway) throws IllegalArgumentException { @Override public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(PublicClaims.EXPIRES_AT, leeway); + customLeeways.put(StandardClaims.EXPIRES_AT, leeway); return this; } @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(PublicClaims.NOT_BEFORE, leeway); + customLeeways.put(StandardClaims.NOT_BEFORE, leeway); return this; } @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(PublicClaims.ISSUED_AT, leeway); + customLeeways.put(StandardClaims.ISSUED_AT, leeway); return this; } @@ -159,7 +158,7 @@ public Verification ignoreIssuedAt() { @Override public Verification withJWTId(String jwtId) { - addCheck(PublicClaims.JWT_ID, ((claim, decodedJWT) -> + addCheck(StandardClaims.JWT_ID, ((claim, decodedJWT) -> verifyNull(claim, jwtId) || jwtId.equals(claim.asString()))); return this; } @@ -297,17 +296,17 @@ public long getLeewayFor(String name) { } private void addMandatoryClaimChecks() { - long expiresAtLeeway = getLeewayFor(PublicClaims.EXPIRES_AT); - long notBeforeLeeway = getLeewayFor(PublicClaims.NOT_BEFORE); - long issuedAtLeeway = getLeewayFor(PublicClaims.ISSUED_AT); - - expectedChecks.add(constructExpectedCheck(PublicClaims.EXPIRES_AT, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.EXPIRES_AT, claim, expiresAtLeeway, true))); - expectedChecks.add(constructExpectedCheck(PublicClaims.NOT_BEFORE, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.NOT_BEFORE, claim, notBeforeLeeway, false))); + long expiresAtLeeway = getLeewayFor(StandardClaims.EXPIRES_AT); + long notBeforeLeeway = getLeewayFor(StandardClaims.NOT_BEFORE); + long issuedAtLeeway = getLeewayFor(StandardClaims.ISSUED_AT); + + expectedChecks.add(constructExpectedCheck(StandardClaims.EXPIRES_AT, (claim, decodedJWT) -> + assertValidInstantClaim(StandardClaims.EXPIRES_AT, claim, expiresAtLeeway, true))); + expectedChecks.add(constructExpectedCheck(StandardClaims.NOT_BEFORE, (claim, decodedJWT) -> + assertValidInstantClaim(StandardClaims.NOT_BEFORE, claim, notBeforeLeeway, false))); if (!ignoreIssuedAt) { - expectedChecks.add(constructExpectedCheck(PublicClaims.ISSUED_AT, (claim, decodedJWT) -> - assertValidInstantClaim(PublicClaims.ISSUED_AT, claim, issuedAtLeeway, false))); + expectedChecks.add(constructExpectedCheck(StandardClaims.ISSUED_AT, (claim, decodedJWT) -> + assertValidInstantClaim(StandardClaims.ISSUED_AT, claim, issuedAtLeeway, false))); } } diff --git a/lib/src/main/java/com/auth0/jwt/StandardClaims.java b/lib/src/main/java/com/auth0/jwt/StandardClaims.java new file mode 100644 index 00000000..4fe82d64 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/StandardClaims.java @@ -0,0 +1,48 @@ +package com.auth0.jwt; + +/** + * Contains constants representing the name of the Registered Claim Names as defined in Section 4.1.1 of + * RFC 7529 + */ +public final class StandardClaims { + + private StandardClaims() { + } + + /** + * The "iss" (issuer) claim identifies the principal that issued the JWT. + */ + public static String ISSUER = "iss"; + + /** + * The "sub" (subject) claim identifies the principal that is the subject of the JWT. + */ + public static String SUBJECT = "sub"; + + /** + * The "aud" (audience) claim identifies the recipients that the JWT is intended for. + */ + public static String AUDIENCE = "aud"; + + /** + * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be + * accepted for processing. + */ + public static String EXPIRES_AT = "exp"; + + /** + * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. + */ + public static String NOT_BEFORE = "nbf"; + + /** + * The "iat" (issued at) claim identifies the time at which the JWT was issued. + */ + public static String ISSUED_AT = "iat"; + + /** + * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + */ + public static String JWT_ID = "jti"; + +} diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java index 8a6cbcf8..9293fd4d 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -1,5 +1,6 @@ package com.auth0.jwt.impl; +import com.auth0.jwt.HeaderParams; import com.auth0.jwt.exceptions.JWTDecodeException; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; @@ -40,10 +41,10 @@ public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws throw new JWTDecodeException("Parsing the Header's JSON resulted on a Null map"); } - String algorithm = getString(tree, PublicClaims.ALGORITHM); - String type = getString(tree, PublicClaims.TYPE); - String contentType = getString(tree, PublicClaims.CONTENT_TYPE); - String keyId = getString(tree, PublicClaims.KEY_ID); + String algorithm = getString(tree, HeaderParams.ALGORITHM); + String type = getString(tree, HeaderParams.TYPE); + String contentType = getString(tree, HeaderParams.CONTENT_TYPE); + String keyId = getString(tree, HeaderParams.KEY_ID); return new BasicHeader(algorithm, type, contentType, keyId, tree, objectReader); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 09009267..4d058826 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -1,5 +1,6 @@ package com.auth0.jwt.impl; +import com.auth0.jwt.StandardClaims; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.core.JsonParser; @@ -43,13 +44,13 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE throw new JWTDecodeException("Parsing the Payload's JSON resulted on a Null map"); } - String issuer = getString(tree, PublicClaims.ISSUER); - String subject = getString(tree, PublicClaims.SUBJECT); - List audience = getStringOrArray(tree, PublicClaims.AUDIENCE); - Instant expiresAt = getInstantFromSeconds(tree, PublicClaims.EXPIRES_AT); - Instant notBefore = getInstantFromSeconds(tree, PublicClaims.NOT_BEFORE); - Instant issuedAt = getInstantFromSeconds(tree, PublicClaims.ISSUED_AT); - String jwtId = getString(tree, PublicClaims.JWT_ID); + String issuer = getString(tree, StandardClaims.ISSUER); + String subject = getString(tree, StandardClaims.SUBJECT); + List audience = getStringOrArray(tree, StandardClaims.AUDIENCE); + Instant expiresAt = getInstantFromSeconds(tree, StandardClaims.EXPIRES_AT); + Instant notBefore = getInstantFromSeconds(tree, StandardClaims.NOT_BEFORE); + Instant issuedAt = getInstantFromSeconds(tree, StandardClaims.ISSUED_AT); + String jwtId = getString(tree, StandardClaims.JWT_ID); return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index dc138c08..1d2ada43 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -1,5 +1,6 @@ package com.auth0.jwt.impl; +import com.auth0.jwt.StandardClaims; import com.fasterxml.jackson.core.JsonGenerator; import java.io.IOException; @@ -22,7 +23,7 @@ public PayloadSerializer() { @Override protected void writeClaim(Map.Entry entry, JsonGenerator gen) throws IOException { - if (PublicClaims.AUDIENCE.equals(entry.getKey())) { + if (StandardClaims.AUDIENCE.equals(entry.getKey())) { writeAudience(gen, entry); } else { super.writeClaim(entry, gen); diff --git a/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java b/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java deleted file mode 100644 index ea166b0d..00000000 --- a/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.auth0.jwt.impl; - -/** - * Contains the claim name for all Public claims. - */ -public interface PublicClaims { - - //Header - String ALGORITHM = "alg"; - String CONTENT_TYPE = "cty"; - String TYPE = "typ"; - String KEY_ID = "kid"; - - //Payload - String ISSUER = "iss"; - String SUBJECT = "sub"; - String EXPIRES_AT = "exp"; - String NOT_BEFORE = "nbf"; - String ISSUED_AT = "iat"; - String JWT_ID = "jti"; - String AUDIENCE = "aud"; - -} diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 3cccbf7b..b28035f8 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -1,7 +1,6 @@ package com.auth0.jwt; import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; import com.fasterxml.jackson.databind.ObjectMapper; @@ -92,7 +91,7 @@ public void shouldReturnBuilderIfNullMapIsProvided() { @Test public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() { Map header = new HashMap<>(); - header.put(PublicClaims.KEY_ID, "xyz"); + header.put(HeaderParams.KEY_ID, "xyz"); String signed = JWTCreator.init() .withKeyId("abc") @@ -102,13 +101,13 @@ public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } @Test public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() { Map header = new HashMap<>(); - header.put(PublicClaims.KEY_ID, "xyz"); + header.put(HeaderParams.KEY_ID, "xyz"); String signed = JWTCreator.init() .withHeader(header) @@ -118,13 +117,13 @@ public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "abc")); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "abc")); } @Test public void shouldRemoveHeaderIfTheValueIsNull() { Map header = new HashMap<>(); - header.put(PublicClaims.KEY_ID, null); + header.put(HeaderParams.KEY_ID, null); header.put("test2", "isSet"); String signed = JWTCreator.init() @@ -135,7 +134,7 @@ public void shouldRemoveHeaderIfTheValueIsNull() { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.isNotPresent(PublicClaims.KEY_ID)); + assertThat(headerJson, JsonMatcher.isNotPresent(HeaderParams.KEY_ID)); assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet")); } @@ -728,7 +727,7 @@ public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() { @Test public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKey() { Map payload = new HashMap<>(); - payload.put(PublicClaims.KEY_ID, "xyz"); + payload.put(HeaderParams.KEY_ID, "xyz"); String jwt = JWTCreator.init() .withKeyId("abc") @@ -738,13 +737,13 @@ public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKe assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.KEY_ID, "xyz")); + assertThat(payloadJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } @Test public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { Map payload = new HashMap<>(); - payload.put(PublicClaims.ISSUER, "xyz"); + payload.put(StandardClaims.ISSUER, "xyz"); String jwt = JWTCreator.init() .withPayload(payload) @@ -754,7 +753,7 @@ public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - assertThat(payloadJson, JsonMatcher.hasEntry(PublicClaims.ISSUER, "abc")); + assertThat(payloadJson, JsonMatcher.hasEntry(StandardClaims.ISSUER, "abc")); } @Test diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index 5ea2b4ef..82dc895c 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -121,7 +121,7 @@ public void shouldGetSignature() { assertThat(jwt.getSignature(), is("XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); } - // Public PublicClaims + // Standard Claims @Test public void shouldGetIssuer() { @@ -208,7 +208,7 @@ public void shouldGetAlgorithm() { assertThat(jwt.getAlgorithm(), is("HS256")); } - //Private PublicClaims + // Private Claims @Test public void shouldGetMissingClaimIfClaimDoesNotExist() { diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 958e89fb..b9f56a2e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -10,8 +10,6 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; import java.time.Clock; import java.time.Instant; @@ -19,8 +17,8 @@ import java.util.Base64; import java.util.Date; -import static org.hamcrest.Matchers.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; public class JWTTest { @@ -199,7 +197,7 @@ public void shouldAcceptECDSA512Algorithm() throws Exception { } - // Public Claims + // Standard Claims @Test public void shouldGetAlgorithm() { diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 72e5656f..d2715262 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -2,7 +2,6 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; -import com.auth0.jwt.impl.PublicClaims; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; @@ -14,7 +13,6 @@ import java.time.Duration; import java.time.Instant; import java.time.ZoneId; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.function.BiPredicate; @@ -92,7 +90,7 @@ public void shouldThrowOnInvalidIssuer() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); - assertThat(e.getClaimName(), is(PublicClaims.ISSUER)); + assertThat(e.getClaimName(), is(StandardClaims.ISSUER)); assertThat(e.getClaimValue().asString(), is("auth0")); } @@ -106,7 +104,7 @@ public void shouldThrowOnNullIssuer() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); - assertThat(e.getClaimName(), is(PublicClaims.ISSUER)); + assertThat(e.getClaimName(), is(StandardClaims.ISSUER)); assertThat(e.getClaimValue().isNull(), is(true)); } @@ -146,7 +144,7 @@ public void shouldThrowOnInvalidSubject() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'sub' value doesn't match the required one.")); - assertThat(e.getClaimName(), is(PublicClaims.SUBJECT)); + assertThat(e.getClaimName(), is(StandardClaims.SUBJECT)); assertThat(e.getClaimValue().asString(), is("1234567890")); } @@ -250,7 +248,7 @@ public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() { .verify(tokenArr); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @@ -265,7 +263,7 @@ public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @@ -280,7 +278,7 @@ public void shouldThrowWhenAudienceClaimIsNull() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); assertThat(e.getClaimValue().isNull(), is(true)); } @@ -308,7 +306,7 @@ public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(PublicClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {null})); } @@ -585,9 +583,9 @@ public void shouldAddDefaultLeewayToDateClaims() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(0L)); - assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(0L)); - assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(0L)); + assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(0L)); + assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(0L)); + assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(0L)); } @Test @@ -599,9 +597,9 @@ public void shouldAddCustomLeewayToDateClaims() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); } @Test @@ -614,9 +612,9 @@ public void shouldOverrideDefaultIssuedAtLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(9999L)); - assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(9999L)); + assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); } @Test @@ -629,9 +627,9 @@ public void shouldOverrideDefaultExpiresAtLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(9999L)); - assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(9999L)); + assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); } @Test @@ -644,9 +642,9 @@ public void shouldOverrideDefaultNotBeforeLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(PublicClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(PublicClaims.NOT_BEFORE), is(9999L)); + assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(9999L)); } @Test @@ -727,7 +725,7 @@ public void shouldThrowOnInvalidNotBeforeIfPresent() { .verify(token); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(PublicClaims.NOT_BEFORE)); + assertThat(e.getClaimName(), is(StandardClaims.NOT_BEFORE)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } @@ -762,7 +760,7 @@ public void shouldThrowOnFutureIssuedAt() { assertThat(jwt, is(notNullValue())); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(PublicClaims.ISSUED_AT)); + assertThat(e.getClaimName(), is(StandardClaims.ISSUED_AT)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } @@ -787,7 +785,7 @@ public void shouldThrowOnInvalidIssuedAtIfPresent() { .verify(token); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(PublicClaims.ISSUED_AT)); + assertThat(e.getClaimName(), is(StandardClaims.ISSUED_AT)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } From 5da2806b592daeef57b37e5d5461de6d0ef2bbe9 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 13 Apr 2022 12:55:40 -0500 Subject: [PATCH 185/293] fix packaging --- lib/src/main/java/{ => com/auth0/jwt}/HeaderParams.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/src/main/java/{ => com/auth0/jwt}/HeaderParams.java (100%) diff --git a/lib/src/main/java/HeaderParams.java b/lib/src/main/java/com/auth0/jwt/HeaderParams.java similarity index 100% rename from lib/src/main/java/HeaderParams.java rename to lib/src/main/java/com/auth0/jwt/HeaderParams.java From 035f323dc4e0c4654c661c050843f521028aec21 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 13 Apr 2022 13:01:04 -0500 Subject: [PATCH 186/293] Update class names --- .../main/java/com/auth0/jwt/JWTCreator.java | 20 ++++---- .../main/java/com/auth0/jwt/JWTVerifier.java | 42 ++++++++-------- ...ndardClaims.java => RegisteredClaims.java} | 4 +- .../auth0/jwt/impl/PayloadDeserializer.java | 16 +++--- .../com/auth0/jwt/impl/PayloadSerializer.java | 4 +- .../java/com/auth0/jwt/JWTCreatorTest.java | 4 +- .../java/com/auth0/jwt/JWTVerifierTest.java | 50 +++++++++---------- 7 files changed, 70 insertions(+), 70 deletions(-) rename lib/src/main/java/com/auth0/jwt/{StandardClaims.java => RegisteredClaims.java} (94%) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index f0299d22..cab474f7 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -115,7 +115,7 @@ public Builder withKeyId(String keyId) { * @return this same Builder instance. */ public Builder withIssuer(String issuer) { - addClaim(StandardClaims.ISSUER, issuer); + addClaim(RegisteredClaims.ISSUER, issuer); return this; } @@ -126,7 +126,7 @@ public Builder withIssuer(String issuer) { * @return this same Builder instance. */ public Builder withSubject(String subject) { - addClaim(StandardClaims.SUBJECT, subject); + addClaim(RegisteredClaims.SUBJECT, subject); return this; } @@ -137,7 +137,7 @@ public Builder withSubject(String subject) { * @return this same Builder instance. */ public Builder withAudience(String... audience) { - addClaim(StandardClaims.AUDIENCE, audience); + addClaim(RegisteredClaims.AUDIENCE, audience); return this; } @@ -149,7 +149,7 @@ public Builder withAudience(String... audience) { * @return this same Builder instance. */ public Builder withExpiresAt(Date expiresAt) { - addClaim(StandardClaims.EXPIRES_AT, expiresAt); + addClaim(RegisteredClaims.EXPIRES_AT, expiresAt); return this; } @@ -161,7 +161,7 @@ public Builder withExpiresAt(Date expiresAt) { * @return this same Builder instance. */ public Builder withExpiresAt(Instant expiresAt) { - addClaim(StandardClaims.EXPIRES_AT, expiresAt); + addClaim(RegisteredClaims.EXPIRES_AT, expiresAt); return this; } @@ -173,7 +173,7 @@ public Builder withExpiresAt(Instant expiresAt) { * @return this same Builder instance. */ public Builder withNotBefore(Date notBefore) { - addClaim(StandardClaims.NOT_BEFORE, notBefore); + addClaim(RegisteredClaims.NOT_BEFORE, notBefore); return this; } @@ -185,7 +185,7 @@ public Builder withNotBefore(Date notBefore) { * @return this same Builder instance. */ public Builder withNotBefore(Instant notBefore) { - addClaim(StandardClaims.NOT_BEFORE, notBefore); + addClaim(RegisteredClaims.NOT_BEFORE, notBefore); return this; } @@ -197,7 +197,7 @@ public Builder withNotBefore(Instant notBefore) { * @return this same Builder instance. */ public Builder withIssuedAt(Date issuedAt) { - addClaim(StandardClaims.ISSUED_AT, issuedAt); + addClaim(RegisteredClaims.ISSUED_AT, issuedAt); return this; } @@ -209,7 +209,7 @@ public Builder withIssuedAt(Date issuedAt) { * @return this same Builder instance. */ public Builder withIssuedAt(Instant issuedAt) { - addClaim(StandardClaims.ISSUED_AT, issuedAt); + addClaim(RegisteredClaims.ISSUED_AT, issuedAt); return this; } @@ -220,7 +220,7 @@ public Builder withIssuedAt(Instant issuedAt) { * @return this same Builder instance. */ public Builder withJWTId(String jwtId) { - addClaim(StandardClaims.JWT_ID, jwtId); + addClaim(RegisteredClaims.JWT_ID, jwtId); return this; } diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 223de7f5..8b4639ac 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -70,13 +70,13 @@ public static class BaseVerification implements Verification { @Override public Verification withIssuer(String... issuer) { List value = isNullOrEmpty(issuer) ? null : Arrays.asList(issuer); - addCheck(StandardClaims.ISSUER, ((claim, decodedJWT) -> { + addCheck(RegisteredClaims.ISSUER, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (value == null || !value.contains(claim.asString())) { throw new IncorrectClaimException( - "The Claim 'iss' value doesn't match the required issuer.", StandardClaims.ISSUER, claim); + "The Claim 'iss' value doesn't match the required issuer.", RegisteredClaims.ISSUER, claim); } return true; })); @@ -85,7 +85,7 @@ public Verification withIssuer(String... issuer) { @Override public Verification withSubject(String subject) { - addCheck(StandardClaims.SUBJECT, (claim, decodedJWT) -> + addCheck(RegisteredClaims.SUBJECT, (claim, decodedJWT) -> verifyNull(claim, subject) || subject.equals(claim.asString())); return this; } @@ -93,13 +93,13 @@ public Verification withSubject(String subject) { @Override public Verification withAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - addCheck(StandardClaims.AUDIENCE, ((claim, decodedJWT) -> { + addCheck(RegisteredClaims.AUDIENCE, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, true)) { throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", - StandardClaims.AUDIENCE, claim); + RegisteredClaims.AUDIENCE, claim); } return true; })); @@ -109,13 +109,13 @@ public Verification withAudience(String... audience) { @Override public Verification withAnyOfAudience(String... audience) { List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); - addCheck(StandardClaims.AUDIENCE, ((claim, decodedJWT) -> { + addCheck(RegisteredClaims.AUDIENCE, ((claim, decodedJWT) -> { if (verifyNull(claim, value)) { return true; } if (!assertValidAudienceClaim(decodedJWT.getAudience(), value, false)) { throw new IncorrectClaimException("The Claim 'aud' value doesn't contain the required audience.", - StandardClaims.AUDIENCE, claim); + RegisteredClaims.AUDIENCE, claim); } return true; })); @@ -132,21 +132,21 @@ public Verification acceptLeeway(long leeway) throws IllegalArgumentException { @Override public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(StandardClaims.EXPIRES_AT, leeway); + customLeeways.put(RegisteredClaims.EXPIRES_AT, leeway); return this; } @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(StandardClaims.NOT_BEFORE, leeway); + customLeeways.put(RegisteredClaims.NOT_BEFORE, leeway); return this; } @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - customLeeways.put(StandardClaims.ISSUED_AT, leeway); + customLeeways.put(RegisteredClaims.ISSUED_AT, leeway); return this; } @@ -158,7 +158,7 @@ public Verification ignoreIssuedAt() { @Override public Verification withJWTId(String jwtId) { - addCheck(StandardClaims.JWT_ID, ((claim, decodedJWT) -> + addCheck(RegisteredClaims.JWT_ID, ((claim, decodedJWT) -> verifyNull(claim, jwtId) || jwtId.equals(claim.asString()))); return this; } @@ -296,17 +296,17 @@ public long getLeewayFor(String name) { } private void addMandatoryClaimChecks() { - long expiresAtLeeway = getLeewayFor(StandardClaims.EXPIRES_AT); - long notBeforeLeeway = getLeewayFor(StandardClaims.NOT_BEFORE); - long issuedAtLeeway = getLeewayFor(StandardClaims.ISSUED_AT); - - expectedChecks.add(constructExpectedCheck(StandardClaims.EXPIRES_AT, (claim, decodedJWT) -> - assertValidInstantClaim(StandardClaims.EXPIRES_AT, claim, expiresAtLeeway, true))); - expectedChecks.add(constructExpectedCheck(StandardClaims.NOT_BEFORE, (claim, decodedJWT) -> - assertValidInstantClaim(StandardClaims.NOT_BEFORE, claim, notBeforeLeeway, false))); + long expiresAtLeeway = getLeewayFor(RegisteredClaims.EXPIRES_AT); + long notBeforeLeeway = getLeewayFor(RegisteredClaims.NOT_BEFORE); + long issuedAtLeeway = getLeewayFor(RegisteredClaims.ISSUED_AT); + + expectedChecks.add(constructExpectedCheck(RegisteredClaims.EXPIRES_AT, (claim, decodedJWT) -> + assertValidInstantClaim(RegisteredClaims.EXPIRES_AT, claim, expiresAtLeeway, true))); + expectedChecks.add(constructExpectedCheck(RegisteredClaims.NOT_BEFORE, (claim, decodedJWT) -> + assertValidInstantClaim(RegisteredClaims.NOT_BEFORE, claim, notBeforeLeeway, false))); if (!ignoreIssuedAt) { - expectedChecks.add(constructExpectedCheck(StandardClaims.ISSUED_AT, (claim, decodedJWT) -> - assertValidInstantClaim(StandardClaims.ISSUED_AT, claim, issuedAtLeeway, false))); + expectedChecks.add(constructExpectedCheck(RegisteredClaims.ISSUED_AT, (claim, decodedJWT) -> + assertValidInstantClaim(RegisteredClaims.ISSUED_AT, claim, issuedAtLeeway, false))); } } diff --git a/lib/src/main/java/com/auth0/jwt/StandardClaims.java b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java similarity index 94% rename from lib/src/main/java/com/auth0/jwt/StandardClaims.java rename to lib/src/main/java/com/auth0/jwt/RegisteredClaims.java index 4fe82d64..d60d9fc5 100644 --- a/lib/src/main/java/com/auth0/jwt/StandardClaims.java +++ b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java @@ -4,9 +4,9 @@ * Contains constants representing the name of the Registered Claim Names as defined in Section 4.1.1 of * RFC 7529 */ -public final class StandardClaims { +public final class RegisteredClaims { - private StandardClaims() { + private RegisteredClaims() { } /** diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 4d058826..37e70f7a 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -1,6 +1,6 @@ package com.auth0.jwt.impl; -import com.auth0.jwt.StandardClaims; +import com.auth0.jwt.RegisteredClaims; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.core.JsonParser; @@ -44,13 +44,13 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE throw new JWTDecodeException("Parsing the Payload's JSON resulted on a Null map"); } - String issuer = getString(tree, StandardClaims.ISSUER); - String subject = getString(tree, StandardClaims.SUBJECT); - List audience = getStringOrArray(tree, StandardClaims.AUDIENCE); - Instant expiresAt = getInstantFromSeconds(tree, StandardClaims.EXPIRES_AT); - Instant notBefore = getInstantFromSeconds(tree, StandardClaims.NOT_BEFORE); - Instant issuedAt = getInstantFromSeconds(tree, StandardClaims.ISSUED_AT); - String jwtId = getString(tree, StandardClaims.JWT_ID); + String issuer = getString(tree, RegisteredClaims.ISSUER); + String subject = getString(tree, RegisteredClaims.SUBJECT); + List audience = getStringOrArray(tree, RegisteredClaims.AUDIENCE); + Instant expiresAt = getInstantFromSeconds(tree, RegisteredClaims.EXPIRES_AT); + Instant notBefore = getInstantFromSeconds(tree, RegisteredClaims.NOT_BEFORE); + Instant issuedAt = getInstantFromSeconds(tree, RegisteredClaims.ISSUED_AT); + String jwtId = getString(tree, RegisteredClaims.JWT_ID); return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index 1d2ada43..c8df2bb9 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -1,6 +1,6 @@ package com.auth0.jwt.impl; -import com.auth0.jwt.StandardClaims; +import com.auth0.jwt.RegisteredClaims; import com.fasterxml.jackson.core.JsonGenerator; import java.io.IOException; @@ -23,7 +23,7 @@ public PayloadSerializer() { @Override protected void writeClaim(Map.Entry entry, JsonGenerator gen) throws IOException { - if (StandardClaims.AUDIENCE.equals(entry.getKey())) { + if (RegisteredClaims.AUDIENCE.equals(entry.getKey())) { writeAudience(gen, entry); } else { super.writeClaim(entry, gen); diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index b28035f8..efe6b94b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -743,7 +743,7 @@ public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKe @Test public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { Map payload = new HashMap<>(); - payload.put(StandardClaims.ISSUER, "xyz"); + payload.put(RegisteredClaims.ISSUER, "xyz"); String jwt = JWTCreator.init() .withPayload(payload) @@ -753,7 +753,7 @@ public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { assertThat(jwt, is(notNullValue())); String[] parts = jwt.split("\\."); String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); - assertThat(payloadJson, JsonMatcher.hasEntry(StandardClaims.ISSUER, "abc")); + assertThat(payloadJson, JsonMatcher.hasEntry(RegisteredClaims.ISSUER, "abc")); } @Test diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index d2715262..6d8ba201 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -90,7 +90,7 @@ public void shouldThrowOnInvalidIssuer() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); - assertThat(e.getClaimName(), is(StandardClaims.ISSUER)); + assertThat(e.getClaimName(), is(RegisteredClaims.ISSUER)); assertThat(e.getClaimValue().asString(), is("auth0")); } @@ -104,7 +104,7 @@ public void shouldThrowOnNullIssuer() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'iss' value doesn't match the required issuer.")); - assertThat(e.getClaimName(), is(StandardClaims.ISSUER)); + assertThat(e.getClaimName(), is(RegisteredClaims.ISSUER)); assertThat(e.getClaimValue().isNull(), is(true)); } @@ -144,7 +144,7 @@ public void shouldThrowOnInvalidSubject() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'sub' value doesn't match the required one.")); - assertThat(e.getClaimName(), is(StandardClaims.SUBJECT)); + assertThat(e.getClaimName(), is(RegisteredClaims.SUBJECT)); assertThat(e.getClaimValue().asString(), is("1234567890")); } @@ -248,7 +248,7 @@ public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() { .verify(tokenArr); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @@ -263,7 +263,7 @@ public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); } @@ -278,7 +278,7 @@ public void shouldThrowWhenAudienceClaimIsNull() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); assertThat(e.getClaimValue().isNull(), is(true)); } @@ -306,7 +306,7 @@ public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { .verify(token); }); assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); - assertThat(e.getClaimName(), is(StandardClaims.AUDIENCE)); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); assertThat(e.getClaimValue().asArray(String.class), is(new String[] {null})); } @@ -583,9 +583,9 @@ public void shouldAddDefaultLeewayToDateClaims() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(0L)); - assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(0L)); - assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(0L)); + assertThat(verification.getLeewayFor(RegisteredClaims.ISSUED_AT), is(0L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(0L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(0L)); } @Test @@ -597,9 +597,9 @@ public void shouldAddCustomLeewayToDateClaims() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } @Test @@ -612,9 +612,9 @@ public void shouldOverrideDefaultIssuedAtLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(9999L)); - assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.ISSUED_AT), is(9999L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } @Test @@ -627,9 +627,9 @@ public void shouldOverrideDefaultExpiresAtLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(9999L)); - assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(9999L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } @Test @@ -642,9 +642,9 @@ public void shouldOverrideDefaultNotBeforeLeeway() { .build(); assertThat(verifier.expectedChecks, is(notNullValue())); - assertThat(verification.getLeewayFor(StandardClaims.ISSUED_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.EXPIRES_AT), is(1234L)); - assertThat(verification.getLeewayFor(StandardClaims.NOT_BEFORE), is(9999L)); + assertThat(verification.getLeewayFor(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(9999L)); } @Test @@ -725,7 +725,7 @@ public void shouldThrowOnInvalidNotBeforeIfPresent() { .verify(token); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(StandardClaims.NOT_BEFORE)); + assertThat(e.getClaimName(), is(RegisteredClaims.NOT_BEFORE)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } @@ -760,7 +760,7 @@ public void shouldThrowOnFutureIssuedAt() { assertThat(jwt, is(notNullValue())); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(StandardClaims.ISSUED_AT)); + assertThat(e.getClaimName(), is(RegisteredClaims.ISSUED_AT)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } @@ -785,7 +785,7 @@ public void shouldThrowOnInvalidIssuedAtIfPresent() { .verify(token); }); assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); - assertThat(e.getClaimName(), is(StandardClaims.ISSUED_AT)); + assertThat(e.getClaimName(), is(RegisteredClaims.ISSUED_AT)); assertThat(e.getClaimValue().asLong(), is(1477592L)); } From 537f9863b8d3297d7aa7660094a690e5909afdf8 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Thu, 14 Apr 2022 16:32:58 -0400 Subject: [PATCH 187/293] Add Semgrep to continuous integration tests This PR updates the CircleCI workflow for v4 to add Semgrep automated security testing to the continuous integration tests. --- .circleci/config.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a299cb9d..14f9652e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -53,11 +53,26 @@ jobs: # GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' # _JAVA_OPTIONS: "-Xms512m -Xmx1024m" # TERM: dumb + semgrep: + docker: + - image: returntocorp/semgrep-agent:v1 + environment: + SEMGREP_REPO_NAME: "auth0/java-jwt" + SEMGREP_REPO_URL: "https://github.com/auth0/java-jwt" + steps: + - checkout + - run: + name: Run vulnerabilities tests (Semgrep) + command: | + semgrep-agent --baseline-ref v4-dev --publish-token $SEMGREP_TOKEN workflows: build-and-test: jobs: - build + - semgrep: + context: + - semgrep-env # api-diff: # jobs: -# - api-diff \ No newline at end of file +# - api-diff From 54d97c68ad2e4b57aa21dea6bc3ed6cec0c0479d Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Thu, 14 Apr 2022 16:34:15 -0400 Subject: [PATCH 188/293] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 14f9652e..f47d6efa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,7 +64,7 @@ jobs: - run: name: Run vulnerabilities tests (Semgrep) command: | - semgrep-agent --baseline-ref v4-dev --publish-token $SEMGREP_TOKEN + semgrep-agent --baseline-ref master --publish-token $SEMGREP_TOKEN workflows: build-and-test: From 5d8776d7e3d14f9d454404813ae0656c3a8edf2d Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 18 Apr 2022 17:04:19 -0500 Subject: [PATCH 189/293] [SDK-3244] Add Migration Guide --- MIGRATION_GUIDE.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 MIGRATION_GUIDE.md diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..1f95f323 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,84 @@ +# Migration Guide + +## Upgrading from v3.x -> v4.0 + +The version 4 release contains several improvements: + +- Support for `java.time.Instant` when creating or verifying JWTs with Numeric Date claim values. +- Improvements to JWT claim validation, including support for custom claim validation using Predicates. +- Improved exception handling when validating JWTs, to better inform of the reason for failed validation. +- Consistent handling of `null` claim values both when creating and validation JWTs. + +This guide captures the changes you should be aware of when planning and upgrading to version 4. + +### Compile or runtime breaking changes + +**Classes or methods removed:** +- The `impl` package has been removed as an export in `module-info.java`. This package contains implementation-specific code that may change at any point. +- Support for the ES256K algorithm has been removed, as it is disabled in Java 15+. The `Algorithm#ECDSA256K(ECDSAKeyProvider keyProvider)` and `Algorithm#ECDSA256K(ECPublicKey publicKey, ECPrivateKey privateKey)` methods have been removed. +- `com.auth0.jwt.interfaces.Clock` has been removed. Instead, an implementation of `java.time.Clock` can be passed to the `BaseVerification` for testing purposes. +- `com.auth0.jwt.impl.NullClaim` has been removed. `Claim#isNull` can be used to determine if a claim's value is `null`. +- `com.auth0.jwt.impl.PublicClaims` was removed, and replaced by `com.auth0.jwt.StandardClaims` and `com.auth0.jwt.HeaderParams`. + +### Behavioral potentially breaking changes + +#### JWT creation + +- All date/time claim values are now serialized as **seconds since the epoch**, in both the payload and header. In version 3, date/time claims nested in a list or map, as well as any header parameters with date/time values, were serialized as milliseconds since the epoch. +- When creating a JWT, passing `null` as the value no longer removes the claim if it was previously added to the builder. It now adds the claim with a `null` value. + +#### JWT validation + +- In version 3, specifying multiple claim expectations for the same claim name would override any previous expectations for that claim. In version 4, all expectations for that claim will be validated. +- In version 3, passing `null` for the value of a claim expectation would remove that expectation from the validation. In version 4, passing `null` does not remove that expectation, but instead validates that the claim has the literal value `null`. +- When validating a JWT, if an expected claim is present in the JWT but contains a value different from the one expected, an `IncorrectClaimException` (subclass of `InvalidClaimException`) will now be thrown instead of an `InvalidClaimException`. +- When validating a JWT, if an expected claim is not present in the JWT, an `MissingClaimException` (subclass of `InvalidClaimException`) will now be thrown instead of an `InvalidClaimException`. +- `withClaimPresence(String claimName)` now validates that the claim is present in the JWT, and a claim with a `null` value is considered present. Previously, a claim with a value of `null` would be considered as missing and fail the validation. +- When validating a date/time claim value, the validation no longer checks for strict equality of the claim's value and the provided `Date` (or `Instant`). Instead, the expected `Date` or `Instant` will be compared to the claim's value only considering seconds (because JWT date/time claims are represented as seconds since the epoch). + +#### Claim changes + +- `com.auth0.jwt.interfaces.Claim#isNull()` now returns true only if the claim is present and its value is `null`. Previously, it returned true if the claim was present and its value was `null`, or if the claim was not present in the JWT. To check if the claim is present or not in the JWT, use `isMissing()`. + +### New classes or methods + +#### `IncorrectClaimException` added + +This class extends `InvalidClaimException` and represents that when validating a JWT, an expected claim exists in the JWT but does not match the expected value. + +#### `MissingClaimException` added + +This class extends `InvalidClaimException` and represents that when validating a JWT, an expected claim is missing from the JWT. + +### `HeaderParams` added + +This class contains constants representing common header parameter names. + +### `StandardClaims` added + +This class contains constants representing the standard claim names. + +#### `JWTCreator` new methods + +- `JWTCreator.Builder#withExpiresAt(Instant expiresAt)` - adds the `exp` claim to the JWT from a `java.time.Instant`. +- `JWTCreator.Builder#withNotBefore(Instant notBefore)` - adds the `nbf` claim to the JWT from a `java.time.Instant`. +- `JWTCreator.Builder#withIssuedAt(Instant issuedAt)` - adds the `iat` claim to the JWT from a `java.time.Instant`. +- `JWTCreator.Builder#withClaim(String claimName, Instant value)` - adds a claim to the JWT from a `java.time.Instant`. +- `JWTCreator.Builder#withNullClaim(String claimName)` - adds a claim to the JWT with the literal value `null`. + +#### `DecodedJWT` new methods + +- `Instant getExpiresAtAsInstant()` - Returns a JWT's `exp` claim as a `java.time.Instant`. +- `Instant getNotBeforeAsInstant()` - Returns a JWT's `nbf` claim as a `java.time.Instant`. +- `Instant getIssuedAtAsInstant()` - Returns a JWT's `iat` claim as a `java.time.Instant`. + +#### `Claim` new methods + +- `Instant asInstant()` - Gets a claim as a `java.time.Instant`. +- `boolean isMissing()` - Returns whether the claim is present or not. + +#### `Verification` new methods + +- `Verification withClaim(String name, Instant value)` - Adds an expectation that a claim with the provided name has a value equal to the provided `java.time.Instant`. +- `Verification withClaim(String name, BiPredicate predicate)` - Allows for a claim to be validated with the supplied predicate. +- `Verification withNullClaim(String name)` - Adds an expectation that a claim with the provided name has a value equal to the literal `null`. From e6565cecb1049c43f5dee0df5a0d545cc369c74e Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 20 Apr 2022 19:05:43 +0530 Subject: [PATCH 190/293] JavaDoc updated --- .../main/java/com/auth0/jwt/HeaderParams.java | 4 +- lib/src/main/java/com/auth0/jwt/JWT.java | 8 +- .../main/java/com/auth0/jwt/JWTCreator.java | 2 + .../main/java/com/auth0/jwt/JWTVerifier.java | 4 +- .../java/com/auth0/jwt/RegisteredClaims.java | 9 +- .../com/auth0/jwt/impl/PayloadSerializer.java | 2 - .../java/com/auth0/jwt/interfaces/Claim.java | 20 ++-- .../java/com/auth0/jwt/interfaces/Header.java | 4 +- .../auth0/jwt/interfaces/JWTPartsParser.java | 6 +- .../com/auth0/jwt/interfaces/JWTVerifier.java | 4 +- .../com/auth0/jwt/interfaces/Payload.java | 6 +- .../auth0/jwt/interfaces/Verification.java | 100 ++++++++---------- .../jwt/interfaces/VerificationTest.java | 5 + 13 files changed, 89 insertions(+), 85 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/HeaderParams.java b/lib/src/main/java/com/auth0/jwt/HeaderParams.java index e15877a2..7af3c442 100644 --- a/lib/src/main/java/com/auth0/jwt/HeaderParams.java +++ b/lib/src/main/java/com/auth0/jwt/HeaderParams.java @@ -13,12 +13,12 @@ private HeaderParams() {} public static String ALGORITHM = "alg"; /** - * The content type of a JWT. + * The content type of the JWT. */ public static String CONTENT_TYPE = "cty"; /** - * The media type of a JWT. + * The media type of the JWT. */ public static String TYPE = "typ"; diff --git a/lib/src/main/java/com/auth0/jwt/JWT.java b/lib/src/main/java/com/auth0/jwt/JWT.java index 9d719d1e..696abe40 100644 --- a/lib/src/main/java/com/auth0/jwt/JWT.java +++ b/lib/src/main/java/com/auth0/jwt/JWT.java @@ -26,7 +26,7 @@ public JWT() { * Decode a given Json Web Token. *

* Note that this method doesn't verify the token's signature! - * Use it only if you trust the token or you already verified it. + * Use it only if you trust the token or if you have already verified it. * * @param token with jwt format as string. * @return a decoded JWT. @@ -41,7 +41,7 @@ public DecodedJWT decodeJwt(String token) throws JWTDecodeException { * Decode a given Json Web Token. *

* Note that this method doesn't verify the token's signature! - * Use it only if you trust the token or you already verified it. + * Use it only if you trust the token or if you have already verified it. * * @param token with jwt format as string. * @return a decoded JWT. @@ -53,10 +53,10 @@ public static DecodedJWT decode(String token) throws JWTDecodeException { } /** - * Returns a {@link JWTVerifier} builder with the algorithm to be used to validate token signature. + * Returns a {@link Verification} builder with the algorithm to be used to validate token signature. * * @param algorithm that will be used to verify the token's signature. - * @return {@link JWTVerifier} builder + * @return {@link Verification} builder * @throws IllegalArgumentException if the provided algorithm is null. */ public static Verification require(Algorithm algorithm) { diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index cab474f7..25624982 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -19,6 +19,8 @@ * from a given Header and Payload content. *

* This class is thread-safe. + * + * TODO Poovam - Should we claim thread safety since KeyProvider implementation can cause signing differ */ @SuppressWarnings("WeakerAccess") public final class JWTCreator { diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 8b4639ac..0bc17fb1 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -35,10 +35,10 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier { } /** - * Initialize a JWTVerifier instance using the given Algorithm. + * Initialize a {@link Verification} instance using the given Algorithm. * * @param algorithm the Algorithm to use on the JWT verification. - * @return a JWTVerifier.Verification instance to configure. + * @return a {@link Verification} instance to configure. * @throws IllegalArgumentException if the provided algorithm is null. */ static Verification init(Algorithm algorithm) throws IllegalArgumentException { diff --git a/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java index d60d9fc5..8c47eb51 100644 --- a/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java +++ b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java @@ -1,7 +1,7 @@ package com.auth0.jwt; /** - * Contains constants representing the name of the Registered Claim Names as defined in Section 4.1.1 of + * Contains constants representing the name of the Registered Claim Names as defined in Section 4.1 of * RFC 7529 */ public final class RegisteredClaims { @@ -11,37 +11,44 @@ private RegisteredClaims() { /** * The "iss" (issuer) claim identifies the principal that issued the JWT. + * Refer RFC 7529 Section 4.1.1 */ public static String ISSUER = "iss"; /** * The "sub" (subject) claim identifies the principal that is the subject of the JWT. + * Refer RFC 7529 Section 4.1.2 */ public static String SUBJECT = "sub"; /** * The "aud" (audience) claim identifies the recipients that the JWT is intended for. + * Refer RFC 7529 Section 4.1.3 */ public static String AUDIENCE = "aud"; /** * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be * accepted for processing. + * Refer RFC 7529 Section 4.1.4 */ public static String EXPIRES_AT = "exp"; /** * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. + * Refer RFC 7529 Section 4.1.5 */ public static String NOT_BEFORE = "nbf"; /** * The "iat" (issued at) claim identifies the time at which the JWT was issued. + * Refer RFC 7529 Section 4.1.6 */ public static String ISSUED_AT = "iat"; /** * The "jti" (JWT ID) claim provides a unique identifier for the JWT. + * Refer RFC 7529 Section 4.1.7 */ public static String JWT_ID = "jti"; diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index c8df2bb9..24fe37b7 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -34,8 +34,6 @@ protected void writeClaim(Map.Entry entry, JsonGenerator gen) th * Audience may be a list of strings or a single string. This is needed to properly handle the aud claim when * added with the {@linkplain com.auth0.jwt.JWTCreator.Builder#withPayload(Map)} method. */ - - // private void writeAudience(JsonGenerator gen, Map.Entry e) throws IOException { if (e.getValue() instanceof String) { gen.writeFieldName(e.getKey()); diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index f6ccfdcd..7c4da4f7 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -22,7 +22,7 @@ public interface Claim { /** * Can be used to verify whether the Claim is found or not. - * This will be true even if the Claim has null value associated to it. + * This will be true even if the Claim has {@code null} value associated to it. * * @return whether this Claim is present or not */ @@ -30,7 +30,7 @@ public interface Claim { /** * Get this Claim as a Boolean. - * If the value isn't of type Boolean or it can't be converted to a Boolean, null will be returned. + * If the value isn't of type Boolean or it can't be converted to a Boolean, {@code null} will be returned. * * @return the value as a Boolean or null. */ @@ -38,7 +38,7 @@ public interface Claim { /** * Get this Claim as an Integer. - * If the value isn't of type Integer or it can't be converted to an Integer, null will be returned. + * If the value isn't of type Integer or it can't be converted to an Integer, {@code null} will be returned. * * @return the value as an Integer or null. */ @@ -46,7 +46,7 @@ public interface Claim { /** * Get this Claim as an Long. - * If the value isn't of type Long or it can't be converted to an Long, null will be returned. + * If the value isn't of type Long or it can't be converted to a Long, {@code null} will be returned. * * @return the value as an Long or null. */ @@ -54,7 +54,7 @@ public interface Claim { /** * Get this Claim as a Double. - * If the value isn't of type Double or it can't be converted to a Double, null will be returned. + * If the value isn't of type Double or it can't be converted to a Double, {@code null} will be returned. * * @return the value as a Double or null. */ @@ -62,7 +62,7 @@ public interface Claim { /** * Get this Claim as a String. - * If the value isn't of type String or it can't be converted to a String, null will be returned. + * If the value isn't of type String or it can't be converted to a String, {@code null} will be returned. * * @return the value as a String or null. */ @@ -70,7 +70,7 @@ public interface Claim { /** * Get this Claim as a Date. - * If the value can't be converted to a Date, null will be returned. + * If the value can't be converted to a Date, {@code null} will be returned. * * @return the value as a Date or null. */ @@ -78,7 +78,7 @@ public interface Claim { /** * Get this Claim as an Instant. - * If the value can't be converted to an Instant, null will be returned. + * If the value can't be converted to an Instant, {@code null} will be returned. * * @return the value as a Date or null. */ @@ -89,7 +89,7 @@ default Instant asInstant() { /** * Get this Claim as an Array of type T. - * If the value isn't an Array, null will be returned. + * If the value isn't an Array, {@code null} will be returned. * * @param type * @param clazz the type class @@ -100,7 +100,7 @@ default Instant asInstant() { /** * Get this Claim as a List of type T. - * If the value isn't an Array, null will be returned. + * If the value isn't an Array, {@code null} will be returned. * * @param type * @param clazz the type class diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Header.java b/lib/src/main/java/com/auth0/jwt/interfaces/Header.java index 030643d0..52b3ba56 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Header.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Header.java @@ -1,7 +1,7 @@ package com.auth0.jwt.interfaces; /** - * The Header class represents the 1st part of the JWT, where the Header value is hold. + * The Header class represents the 1st part of the JWT, where the Header value is held. */ public interface Header { @@ -35,7 +35,7 @@ public interface Header { /** * Get a Private Claim given it's name. If the Claim wasn't specified in the Header, a 'null claim' will be - * returned. All of the methods of that claim will return {@code null}. + * returned. All the methods of that claim will return {@code null}. * * @param name the name of the Claim to retrieve. * @return a non-null Claim. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java index e1c6efcc..33cd0d70 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java @@ -4,12 +4,12 @@ /** * The JWTPartsParser class defines which parts of the JWT should be converted - * to it's specific Object representation instance. + * to its specific Object representation instance. */ public interface JWTPartsParser { /** - * Parses the given JSON into a Payload instance. + * Parses the given JSON into a {@link Payload} instance. * * @param json the content of the Payload in a JSON representation. * @return the Payload. @@ -18,7 +18,7 @@ public interface JWTPartsParser { Payload parsePayload(String json) throws JWTDecodeException; /** - * Parses the given JSON into a Header instance. + * Parses the given JSON into a {@link Header} instance. * * @param json the content of the Header in a JSON representation. * @return the Header. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index 3555878e..b7030a97 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -4,7 +4,7 @@ /** - * Used to verify the JWT for it's signature and claims. + * Used to verify the JWT for its signature and claims. */ public interface JWTVerifier { @@ -18,7 +18,7 @@ public interface JWTVerifier { DecodedJWT verify(String token) throws JWTVerificationException; /** - * Performs the verification against the given decoded JWT. + * Performs the verification against the given {@link DecodedJWT}. * * @param jwt to verify. * @return a verified and decoded JWT. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java index b3bf71f5..feb58c64 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java @@ -6,7 +6,7 @@ import java.util.Map; /** - * The Payload class represents the 2nd part of the JWT, where the Payload value is hold. + * The Payload class represents the 2nd part of the JWT, where the Payload value is held. */ public interface Payload { @@ -87,8 +87,8 @@ default Instant getIssuedAtAsInstant() { String getId(); /** - * Get a Claim given it's name. If the Claim wasn't specified in the Payload, a 'null claim' - * will be returned. All of the methods of that claim will return {@code null}. + * Get a Claim given its name. If the Claim wasn't specified in the Payload, a 'null claim' + * will be returned. All the methods of that claim will return {@code null}. * * @param name the name of the Claim to retrieve. * @return a non-null Claim. diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 8b0416dc..4a8a0f84 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -7,12 +7,13 @@ import java.util.function.BiPredicate; /** - * Holds the Claims and claim-based configurations required for a JWT to be considered valid. + * Constructs and holds the checks required for a JWT to be considered valid. */ public interface Verification { /** - * Require a specific Issuer ("iss") claim. + * Verifies whether the JWT contains an Issuer ("iss") claim that equals to the value provided. + * This check is case-sensitive. * * @param issuer the required Issuer value. * @return this same Verification instance. @@ -22,7 +23,8 @@ default Verification withIssuer(String issuer) { } /** - * Require a specific Issuer ("iss") claim. + * Verifies whether the JWT contains an Issuer ("iss") claim that contains all the values provided. + * This check is case-sensitive. An empty array is considered as a {@code null}. * * @param issuer the required Issuer value. If multiple values are given, the claim must at least match one of them * @return this same Verification instance. @@ -30,7 +32,8 @@ default Verification withIssuer(String issuer) { Verification withIssuer(String... issuer); /** - * Require a specific Subject ("sub") claim. + * Verifies whether the JWT contains a Subject ("sub") claim that equals to the value provided. + * This check is case-sensitive. * * @param subject the required Subject value * @return this same Verification instance. @@ -38,11 +41,8 @@ default Verification withIssuer(String issuer) { Verification withSubject(String subject); /** - * Require a specific Audience ("aud") claim. If multiple audiences are specified, they must all be present - * in the "aud" claim. - * - * If this is used in conjunction with {@link #withAnyOfAudience(String...)}, whichever one is configured last will - * determine the audience validation behavior. + * Verifies whether the JWT contains an Audience ("aud") claim that contains all the values provided. + * This check is case-sensitive. An empty array is considered as a {@code null}. * * @param audience the required Audience value * @return this same Verification instance. @@ -50,24 +50,13 @@ default Verification withIssuer(String issuer) { Verification withAudience(String... audience); /** - * Require that the Audience ("aud") claim contain at least one of the specified audiences. - * - * If this is used in conjunction with {@link #withAudience(String...)}, whichever one is configured last will - * determine the audience validation behavior. - * - * Note: This method was added after the interface was released. - * It is defined as a default method for compatibility reasons. - * From version 4.0 on, the method will be abstract and all implementations of this interface - * will have to provide their own implementation. - * - * The default implementation throws an {@linkplain UnsupportedOperationException}. + * Verifies whether the JWT contains an Audience ("aud") claim contain at least one of the specified audiences. + * This check is case-sensitive. An empty array is considered as a {@code null}. * * @param audience the required Audience value for which the "aud" claim must contain at least one value. * @return this same Verification instance. */ - default Verification withAnyOfAudience(String... audience) { - throw new UnsupportedOperationException("withAnyOfAudience"); - } + Verification withAnyOfAudience(String... audience); /** * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims @@ -116,149 +105,152 @@ default Verification withAnyOfAudience(String... audience) { Verification acceptIssuedAt(long leeway) throws IllegalArgumentException; /** - * Require a specific JWT Id ("jti") claim. + * Verifies whether the JWT contains a JWT ID ("jti") claim that equals to the value provided. + * This check is case-sensitive. * - * @param jwtId the required Id value + * @param jwtId the required ID value * @return this same Verification instance. */ Verification withJWTId(String jwtId); /** - * Require a claim to be present, with any value. + * Verifies whether the claim is present in the JWT, with any value including {@code null}. * * @param name the Claim's name. * @return this same Verification instance - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaimPresence(String name) throws IllegalArgumentException; /** - * Require a specific Claim value to be null. + * Verifies whether the claim is present with a {@code null} value. * * @param name the Claim's name. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withNullClaim(String name) throws IllegalArgumentException; /** - * Require a specific Claim value. + * Verifies whether the claim is equal to the given Boolean value. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, Boolean value) throws IllegalArgumentException; /** - * Require a specific Claim value. + * Verifies whether the claim is equal to the given Integer value. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, Integer value) throws IllegalArgumentException; /** - * Require a specific Claim value. + * Verifies whether the claim is equal to the given Long value. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, Long value) throws IllegalArgumentException; /** - * Require a specific Claim value. + * Verifies whether the claim is equal to the given Integer value. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, Double value) throws IllegalArgumentException; /** - * Require a specific Claim value. + * Verifies whether the claim is equal to the given String value. + * This check is case-sensitive. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, String value) throws IllegalArgumentException; /** - * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; + * Verifies whether the claim is equal to the given Date value. + * Note that date-time claims are serialized as seconds since the epoch; * when verifying date-time claim value, any time units more granular than seconds will not be considered. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, Date value) throws IllegalArgumentException; /** - * Require a specific Claim value. Note that date-time claims are serialized as seconds since the epoch; + * Verifies whether the claim is equal to the given Instant value. + * Note that date-time claims are serialized as seconds since the epoch; * when verifying a date-time claim value, any time units more granular than seconds will not be considered. * * @param name the Claim's name. * @param value the Claim's value. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ default Verification withClaim(String name, Instant value) throws IllegalArgumentException { return withClaim(name, value != null ? Date.from(value) : null); } /** - * Executes the predicate provided during the verification - * and passes the verification if the predicate returns true. + * Executes the predicate provided and the validates the JWT if the predicate returns true. * * @param name the Claim's name * @param predicate the predicate check to be done. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException; /** - * Require a specific Array Claim to contain at least the given items. + * Verifies whether the claim contain at least the given String items. * * @param name the Claim's name. * @param items the items the Claim must contain. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withArrayClaim(String name, String... items) throws IllegalArgumentException; /** - * Require a specific Array Claim to contain at least the given items. + * Verifies whether the claim contain at least the given Integer items. * * @param name the Claim's name. * @param items the items the Claim must contain. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; /** - * Require a specific Array Claim to contain at least the given items. + * Verifies whether the claim contain at least the given Long items. * * @param name the Claim's name. * @param items the items the Claim must contain. * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is {@code null}. */ Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException; /** - * Skip the Issued At ("iat") date verification. By default, the verification is performed. + * Skip the Issued At ("iat") claim verification. By default, the verification is performed. * * @return this same Verification instance. */ @@ -267,7 +259,7 @@ default Verification withClaim(String name, Instant value) throws IllegalArgumen /** * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. * - * @return a new JWTVerifier instance. + * @return a new {@link com.auth0.jwt.interfaces.JWTVerifier} instance. */ JWTVerifier build(); } diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java index fb0c74a9..bdbd688a 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java @@ -74,6 +74,11 @@ public Verification withAudience(String... audience) { return null; } + @Override + public Verification withAnyOfAudience(String... audience) { + return null; + } + @Override public Verification acceptLeeway(long leeway) throws IllegalArgumentException { return null; From 1979b95f272cc4bffda318a7db097bbed66a5d82 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 20 Apr 2022 19:45:09 +0530 Subject: [PATCH 191/293] Removed test for default audience claim check --- .../java/com/auth0/jwt/interfaces/VerificationTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java index bdbd688a..11acb029 100644 --- a/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java +++ b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java @@ -38,13 +38,6 @@ public void withInstantClaimShouldUseDefaultImplAndHandleNull() { assertThat(((VerificationImplForTest)verification).expectedClaims, hasEntry("name", null)); } - @Test - public void withAnyOfAudienceDeafultImplShouldThrow() { - assertThrows("withAnyOfAudience", UnsupportedOperationException.class, () -> { - new VerificationImplForTest().withAnyOfAudience(""); - }); - } - @Test public void withIssuerStringDefaultImplShouldDelegate() { Verification verification = new VerificationImplForTest() From fef52cc0bd7b1039436ebe09abbe569123812b02 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Wed, 20 Apr 2022 17:27:14 -0500 Subject: [PATCH 192/293] Formatting fixes --- MIGRATION_GUIDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 1f95f323..7105742d 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -50,11 +50,11 @@ This class extends `InvalidClaimException` and represents that when validating a This class extends `InvalidClaimException` and represents that when validating a JWT, an expected claim is missing from the JWT. -### `HeaderParams` added +#### `HeaderParams` added This class contains constants representing common header parameter names. -### `StandardClaims` added +#### `StandardClaims` added This class contains constants representing the standard claim names. From 3f939842f1d0c6c393b9fd6d2365fa2e45d986cc Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Thu, 21 Apr 2022 18:49:01 +0530 Subject: [PATCH 193/293] Added note on Keyprovider --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 2 -- lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 25624982..cab474f7 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -19,8 +19,6 @@ * from a given Header and Payload content. *

* This class is thread-safe. - * - * TODO Poovam - Should we claim thread safety since KeyProvider implementation can cause signing differ */ @SuppressWarnings("WeakerAccess") public final class JWTCreator { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java index fd8baea5..30a144a6 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java @@ -5,6 +5,7 @@ /** * Generic Public/Private Key provider. + * While implementing, ensure the Private Key and Private Key ID doesn't change in between signing a token. * * @param the class that represents the Public Key * @param the class that represents the Private Key From 0d3864da70b1831d4a923450d41cd784ba181c91 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Thu, 21 Apr 2022 10:31:43 -0500 Subject: [PATCH 194/293] updates from PR review --- MIGRATION_GUIDE.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 7105742d..c47b95c2 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -18,7 +18,8 @@ This guide captures the changes you should be aware of when planning and upgradi - Support for the ES256K algorithm has been removed, as it is disabled in Java 15+. The `Algorithm#ECDSA256K(ECDSAKeyProvider keyProvider)` and `Algorithm#ECDSA256K(ECPublicKey publicKey, ECPrivateKey privateKey)` methods have been removed. - `com.auth0.jwt.interfaces.Clock` has been removed. Instead, an implementation of `java.time.Clock` can be passed to the `BaseVerification` for testing purposes. - `com.auth0.jwt.impl.NullClaim` has been removed. `Claim#isNull` can be used to determine if a claim's value is `null`. -- `com.auth0.jwt.impl.PublicClaims` was removed, and replaced by `com.auth0.jwt.StandardClaims` and `com.auth0.jwt.HeaderParams`. +- `com.auth0.jwt.impl.PublicClaims` was removed, and replaced by `com.auth0.jwt.RegisteredClaims` and `com.auth0.jwt.HeaderParams`. +- `com.auth0.jwt.interfaces.Verification#withAnyOfAudience` no longer provides a default implementation. ### Behavioral potentially breaking changes @@ -54,9 +55,9 @@ This class extends `InvalidClaimException` and represents that when validating a This class contains constants representing common header parameter names. -#### `StandardClaims` added +#### `RegisteredClaims` added -This class contains constants representing the standard claim names. +This class contains constants representing the registered claim names. #### `JWTCreator` new methods From b6a9a034b3ceac2e644308e376c0f48aefdcc6da Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 25 Apr 2022 19:31:01 -0500 Subject: [PATCH 195/293] Documentation updates --- .codecov.yml | 1 + README.md | 39 ++++++++++++++++--- .../main/java/com/auth0/jwt/JWTCreator.java | 2 +- .../java/com/auth0/jwt/JWTCreatorTest.java | 6 +-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 51f857af..de278af2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -11,5 +11,6 @@ coverage: project: default: target: auto + threshold: 1% if_no_uploads: error comment: false \ No newline at end of file diff --git a/README.md b/README.md index 065899f1..624a7f68 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,15 @@ A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.or If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. -> This library requires Java 8 or higher. The last version that supported Java 7 was 3.11.0. +> You are viewing the documentation for the v4 beta release. For the latest stable release, please see the [version 3.x documentation](https://github.com/auth0/java-jwt). ## Table of Contents +- [**Requirements**](#requirements) - [**Installation**](#installation) - [**Available Algorithms**](#available-algorithms) - [**Quickstart**](#quickstart) + [**Create and Sign a Token**](#create-and-sign-a-token) + [**Verify a Token**](#verify-a-token) - + [**Decode a Token**](#decode-a-token) - [**Usage**](#usage) + [**Pick the algorithm**](#pick-the-algorithm) + [**Time Validation**](#time-validation) @@ -28,6 +28,10 @@ If you're looking for an **Android** version of the JWT Decoder take a look at o + [**Claim Class**](#claim-class) - [**Javadoc**](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) +## Requirements + +This library is supported for Java 8, 11, and 17. For issues on non-LTS versions above 8, consideration will be given on a case-by-case basis. + ## Installation ### Gradle @@ -138,8 +142,13 @@ You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise. +

+Need to peek into a JWT without verifying it? (Click to expand) + ### Decode a Token +> __Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `JWTVerifier` as documented above instead. + ```java String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; try { @@ -151,6 +160,8 @@ try { If the token has an invalid syntax or the header or payload are not JSONs, a `JWTDecodeException` will raise. +
+ ## Usage ### Pick the Algorithm @@ -176,7 +187,7 @@ Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); ##### HMAC Key Length and Security -When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recomendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. +When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recommendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. #### Using a KeyProvider: By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: @@ -243,13 +254,13 @@ JWTVerifier verifier = JWT.require(algorithm) .build(); ``` -If you need to test this behaviour in your lib/app cast the `Verification` instance to a `BaseVerification` to gain visibility of the `verification.build()` method that accepts a custom `Clock`. e.g.: +If you need to test this behavior in your lib/app cast the `Verification` instance to a `BaseVerification` to gain visibility of the `verification.build()` method that accepts a `java.time.Clock`. e.g.: ```java BaseVerification verification = (BaseVerification) JWT.require(algorithm) .acceptLeeway(1) .acceptExpiresAt(5); -Clock clock = new CustomClock(); //Must implement Clock interface +private final Clock mockNow = Clock.fixed(Instant.ofEpochSecond(1477592), ZoneId.of("UTC")); JWTVerifier verifier = verification.build(clock); ``` @@ -342,6 +353,12 @@ Returns the Expiration Time value or null if it's not defined in the Payload. Date expiresAt = jwt.getExpiresAt(); ``` +If you prefer to work with `java.time.Instant` instead of `java.util.Date`: + +```java +Instant expiresAt = jwt.getExpiresAtAsInstant(); +``` + #### Not Before ("nbf") Returns the Not Before value or null if it's not defined in the Payload. @@ -350,6 +367,12 @@ Returns the Not Before value or null if it's not defined in the Payload. Date notBefore = jwt.getNotBefore(); ``` +If you prefer to work with `java.time.Instant` instead of `java.util.Date`: + +```java +Instant notBefore = jwt.getNotBeforeAsInstant(); +``` + #### Issued At ("iat") Returns the Issued At value or null if it's not defined in the Payload. @@ -358,6 +381,12 @@ Returns the Issued At value or null if it's not defined in the Payload. Date issuedAt = jwt.getIssuedAt(); ``` +If you prefer to work with `java.time.Instant` instead of `java.util.Date`: + +```java +Instant issuedAt = jwt.getIssuedAtAsInstant(); +``` + #### JWT ID ("jti") Returns the JWT ID value or null if it's not defined in the Payload. diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index cab474f7..a99f0fa0 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -453,7 +453,7 @@ public Builder withPayload(Map payloadClaims) throws IllegalArgumentE if (!validatePayload(payloadClaims)) { throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, " - + "Long, Double, String, Date and Null"); + + "Long, Double, String, Date, Instant, and Null"); } // add claims only after validating all claims so as not to corrupt the claims map of this builder diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index efe6b94b..4d1c7314 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -759,7 +759,7 @@ public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { @Test public void withPayloadShouldNotAllowCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date, Instant, and Null"); Map payload = new HashMap<>(); payload.put("entry", "value"); @@ -786,7 +786,7 @@ public void withPayloadShouldAllowNullListItems() { @Test public void withPayloadShouldNotAllowListWithCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date, Instant, and Null"); Map payload = new HashMap<>(); payload.put("list", Arrays.asList("item1", new UserPojo("name", 42))); @@ -798,7 +798,7 @@ public void withPayloadShouldNotAllowListWithCustomType() { @Test public void withPayloadShouldNotAllowMapWithCustomType() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date and Null"); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, Date, Instant, and Null"); Map payload = new HashMap<>(); payload.put("entry", "value"); From 05b54499be0cc06478467d66c801d9bc3da3fecc Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 3 May 2022 15:46:47 +0200 Subject: [PATCH 196/293] Added protection against CVE-2022-21449 --- .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 69 ++++++++- .../jwt/algorithms/ECDSAAlgorithmTest.java | 139 +++++++++++++++++- .../ECDSABouncyCastleProviderTests.java | 20 ++- 3 files changed, 221 insertions(+), 7 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index c2d08a15..f96fd576 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -5,6 +5,7 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; @@ -46,6 +47,7 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { if (publicKey == null) { throw new IllegalStateException("The given Public Key is null."); } + validateSignatureStructure(signatureBytes, publicKey); boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); if (!valid) { @@ -140,13 +142,42 @@ byte[] DERToJOSE(byte[] derSignature) throws SignatureException { return joseSignature; } - //Visible for testing - byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { + /** + * Added check for extra protection against CVE-2022-21449. + * This method ensures the signature's structure is as expected. + * + * @param joseSignature is the signature from the JWT + * @param publicKey public key used to verify the JWT + * @throws SignatureException if the signature's structure is not as per expectation + */ + // Visible for testing + void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) throws SignatureException { + // check signature length, moved this check from JOSEToDER method if (joseSignature.length != ecNumberSize * 2) { throw new SignatureException("Invalid JOSE signature format."); } - // Retrieve R and S number's length and padding. + if (isAllZeros(joseSignature)) { + throw new SignatureException("Invalid Signature: All Zeros."); + } + + // get R + byte[] rBytes = new byte[ecNumberSize]; + System.arraycopy(joseSignature, 0, rBytes, 0, ecNumberSize); + BigInteger r = new BigInteger(1, rBytes); + if(isAllZeros(rBytes)) { + throw new SignatureException("Invalid Signature: All Zeros for R value."); + } + + // get S + byte[] sBytes = new byte[ecNumberSize]; + System.arraycopy(joseSignature, ecNumberSize, sBytes, 0, ecNumberSize); + BigInteger s = new BigInteger(1, sBytes); + if(isAllZeros(sBytes)) { + throw new SignatureException("Invalid Signature: All Zeros for S value."); + } + + //moved this check from JOSEToDER method int rPadding = countPadding(joseSignature, 0, ecNumberSize); int sPadding = countPadding(joseSignature, ecNumberSize, joseSignature.length); int rLength = ecNumberSize - rPadding; @@ -157,6 +188,29 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { throw new SignatureException("Invalid JOSE signature format."); } + // check for 0 r or s here since we have the values + if (BigInteger.ZERO.equals(r) || BigInteger.ZERO.equals(s)) { + throw new SignatureException("R or S value cannot be zero."); + } + + BigInteger order = publicKey.getParams().getOrder(); + + // R and S must be less than N + if (order.compareTo(r) < 1 || order.compareTo(s) < 1) { + throw new SignatureException("The difference between R or S value and order should be greater than one."); + } + } + + //Visible for testing + byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { + // Retrieve R and S number's length and padding. + int rPadding = countPadding(joseSignature, 0, ecNumberSize); + int sPadding = countPadding(joseSignature, ecNumberSize, joseSignature.length); + int rLength = ecNumberSize - rPadding; + int sLength = ecNumberSize - sPadding; + + int length = 2 + rLength + 2 + sLength; + final byte[] derSignature; int offset; if (length > 0x7f) { @@ -205,6 +259,15 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { return derSignature; } + private boolean isAllZeros(byte[] bytes) { + for (byte b : bytes) { + if (b != 0) { + return false; + } + } + return true; + } + private int countPadding(byte[] bytes, int fromIndex, int toIndex) { int padding = 0; while (fromIndex + padding < toIndex && bytes[fromIndex + padding] == 0) { diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 39c9dabf..aee8238c 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -4,6 +4,7 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.JWTVerifier; import org.hamcrest.Matchers; import org.hamcrest.collection.IsIn; import org.junit.Assert; @@ -12,11 +13,13 @@ import org.junit.rules.ExpectedException; import java.io.ByteArrayOutputStream; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.ECKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; import java.util.Arrays; import java.util.Base64; @@ -574,6 +577,10 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -592,6 +599,10 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -610,6 +621,10 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -939,12 +954,13 @@ public void shouldThrowOnDERSignatureConversionIfSNumberDoesNotHaveExpectedLengt @Test public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() throws Exception { - ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); byte[] joseSignature = new byte[32 * 2 - 1]; exception.expect(SignatureException.class); exception.expectMessage("Invalid JOSE signature format."); - algorithm256.JOSEToDER(joseSignature); + algorithm256.validateSignatureStructure(joseSignature, publicKey); } @Test @@ -1309,4 +1325,123 @@ public void shouldFailOnECDSA256SigningWithDeprecatedMethodWhenProvidedPrivateKe algorithm.sign(new byte[0]); } + @Test + public void invalidECDSA256SignatureShouldFailTokenVerification() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(SignatureException.class)); + + String jwtWithInvalidSig = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0._____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ"; + + ECKey key256 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECKey key384 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + ECKey key512 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + ECKey key256k = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); + JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); + JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); + JWTVerifier verifier256k = JWT.require(Algorithm.ECDSA256(key256k)).build(); + verifier256.verify(jwtWithInvalidSig); + verifier384.verify(jwtWithInvalidSig); + verifier512.verify(jwtWithInvalidSig); + verifier256k.verify(jwtWithInvalidSig); + } + + @Test + public void emptyECDSA256SignatureShouldFailTokenVerification() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectCause(isA(SignatureException.class)); + + String jwtWithInvalidSig = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + + ECKey key256 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECKey key384 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + ECKey key512 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + ECKey key256k = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); + JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); + JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); + JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); + JWTVerifier verifier256k = JWT.require(Algorithm.ECDSA256(key256k)).build(); + verifier256.verify(jwtWithInvalidSig); + verifier384.verify(jwtWithInvalidSig); + verifier512.verify(jwtWithInvalidSig); + verifier256k.verify(jwtWithInvalidSig); + } + + @Test + public void signatureWithAllZerosShouldFail() throws Exception { + exception.expect(SignatureException.class); + + ECPublicKey pubKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(pubKey, (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] signatureBytes = new byte[64]; + algorithm256.validateSignatureStructure(signatureBytes, pubKey); + } + + @Test + public void signatureWithRZeroShouldFail() throws Exception { + exception.expect(SignatureException.class); + + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + + String signedJwt = JWT.create().sign(Algorithm.ECDSA256(publicKey, privateKey)); + + String[] chunks = signedJwt.split("\\."); + byte[] signature = Base64.getUrlDecoder().decode(chunks[2]); + + byte[] sigWithBlankR = new byte[signature.length]; + for (int i = 0; i < signature.length; i++) { + if (i < signature.length / 2) { + sigWithBlankR[i] = 0; + } else { + sigWithBlankR[i] = signature[i]; + } + } + + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, privateKey); + algorithm256.validateSignatureStructure(sigWithBlankR, publicKey); + } + + @Test + public void signatureWithSZeroShouldFail() throws Exception { + exception.expect(SignatureException.class); + + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + + String signedJwt = JWT.create().sign(Algorithm.ECDSA256(publicKey, privateKey)); + + String[] chunks = signedJwt.split("\\."); + byte[] signature = Base64.getUrlDecoder().decode(chunks[2]); + + byte[] sigWithBlankS = new byte[signature.length]; + for (int i = 0; i < signature.length; i++) { + if (i < signature.length / 2) { + sigWithBlankS[i] = signature[i]; + } else { + sigWithBlankS[i] = 0; + } + } + + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, privateKey); + algorithm256.validateSignatureStructure(sigWithBlankS, publicKey); + } + + @Test + public void signatureWithRSValueNotLessThanOrderShouldFail() throws Exception { + exception.expect(SignatureException.class); + + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + + String signedJwt = JWT.create().sign(Algorithm.ECDSA256(publicKey, privateKey)); + String jwtWithInvalidSig = signedJwt.substring(0, signedJwt.lastIndexOf('.') + 1) + "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ"; + + String[] chunks = jwtWithInvalidSig.split("\\."); + byte[] invalidSignature = Base64.getUrlDecoder().decode(chunks[2]); + + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, privateKey); + algorithm256.validateSignatureStructure(invalidSignature, publicKey); + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java index 68fe6e50..78e0063d 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -11,11 +11,14 @@ import org.junit.Test; import org.junit.rules.ExpectedException; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.interfaces.ECKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.util.Arrays; import java.util.Base64; import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; @@ -591,6 +594,10 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -609,6 +616,10 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -627,6 +638,10 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + when(publicKey.getParams()).thenReturn(mock(ECParameterSpec.class)); + byte[] a = new byte[64]; + Arrays.fill(a, Byte.MAX_VALUE); + when(publicKey.getParams().getOrder()).thenReturn(new BigInteger(a)); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); @@ -935,12 +950,13 @@ public void shouldThrowOnDERSignatureConversionIfSNumberDoesNotHaveExpectedLengt @Test public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() throws Exception { - ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); byte[] joseSignature = new byte[32 * 2 - 1]; exception.expect(SignatureException.class); exception.expectMessage("Invalid JOSE signature format."); - algorithm256.JOSEToDER(joseSignature); + algorithm256.validateSignatureStructure(joseSignature, publicKey); } @Test From 69dae3eba9235a45e01ab588d5027b73aef918cc Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 3 May 2022 16:16:36 +0200 Subject: [PATCH 197/293] Added banner to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 260f6d6e..0e6a7d48 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). +> :warning: **Important security note:** JVM has a critical vulnerability for ECDSA Algorithms - [CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449). Please review the details of the vulnerability and update your environment. + If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. > This library requires Java 8 or higher. The last version that supported Java 7 was 3.11.0. From 3135ba7893ea60882500fa509c6d98fe48a4f0f8 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 3 May 2022 16:18:01 +0200 Subject: [PATCH 198/293] Removed zero check since it is already verified --- .../main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index f96fd576..1d2544f6 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -188,11 +188,6 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr throw new SignatureException("Invalid JOSE signature format."); } - // check for 0 r or s here since we have the values - if (BigInteger.ZERO.equals(r) || BigInteger.ZERO.equals(s)) { - throw new SignatureException("R or S value cannot be zero."); - } - BigInteger order = publicKey.getParams().getOrder(); // R and S must be less than N From 208d7b66c281fcd91dbc5d83fe51d676ee5e0d51 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 3 May 2022 18:37:52 +0200 Subject: [PATCH 199/293] Improved order comparison --- .../auth0/jwt/algorithms/ECDSAAlgorithm.java | 8 ++++++-- .../jwt/algorithms/ECDSAAlgorithmTest.java | 20 ++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 1d2544f6..00b68cad 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -191,8 +191,12 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr BigInteger order = publicKey.getParams().getOrder(); // R and S must be less than N - if (order.compareTo(r) < 1 || order.compareTo(s) < 1) { - throw new SignatureException("The difference between R or S value and order should be greater than one."); + if (order.compareTo(r) < 1) { + throw new SignatureException("The difference between R value and order should be greater than one."); + } + + if (order.compareTo(s) < 1){ + throw new SignatureException("The difference between S value and order should be greater than one."); } } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index aee8238c..a02c639b 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -1429,7 +1429,7 @@ public void signatureWithSZeroShouldFail() throws Exception { } @Test - public void signatureWithRSValueNotLessThanOrderShouldFail() throws Exception { + public void signatureWithRValueNotLessThanOrderShouldFail() throws Exception { exception.expect(SignatureException.class); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); @@ -1444,4 +1444,22 @@ public void signatureWithRSValueNotLessThanOrderShouldFail() throws Exception { ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, privateKey); algorithm256.validateSignatureStructure(invalidSignature, publicKey); } + + @Test + public void signatureWithSValueNotLessThanOrderShouldFail() throws Exception { + exception.expect(SignatureException.class); + + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + + String signedJwt = JWT.create().sign(Algorithm.ECDSA256(publicKey, privateKey)); + String jwtWithInvalidSig = signedJwt.substring(0, signedJwt.lastIndexOf('.') + 1) + "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ"; + + String[] chunks = jwtWithInvalidSig.split("\\."); + byte[] invalidSignature = Base64.getUrlDecoder().decode(chunks[2]); + invalidSignature[0] = Byte.MAX_VALUE; + + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256(publicKey, privateKey); + algorithm256.validateSignatureStructure(invalidSignature, publicKey); + } } \ No newline at end of file From df004b1bc55ed42f9ccb1d2b820bad08879ad273 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 4 May 2022 11:38:43 +0200 Subject: [PATCH 200/293] Updated documentation regarding HMAC Key length --- README.md | 8 ++++---- .../com/auth0/jwt/algorithms/Algorithm.java | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 624a7f68..3e270500 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` ```java String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; try { - Algorithm algorithm = Algorithm.HMAC256("secret"); + Algorithm algorithm = Algorithm.HMAC256("secret"); //use more secure key JWTVerifier verifier = JWT.require(algorithm) .withIssuer("auth0") .build(); //Reusable verifier instance @@ -175,7 +175,7 @@ When using RSA or ECDSA algorithms and you just need to **sign** JWTs you can av ```java //HMAC -Algorithm algorithmHS = Algorithm.HMAC256("secret"); +Algorithm algorithmHS = Algorithm.HMAC256("secret"); //use more secure key //RSA RSAPublicKey publicKey = //Get the key instance @@ -185,9 +185,9 @@ Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); > Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). -##### HMAC Key Length and Security +##### :key: HMAC Key Length and Security -When using a Hash-based Message Authenticaton Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recommendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. +When using a Hash-based Message Authentication Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recommendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. #### Using a KeyProvider: By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index a5247005..27d79909 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -130,7 +130,9 @@ public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { /** * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". * - * @param secret the secret to use in the verify or signing instance. + * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 256 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -142,6 +144,8 @@ public static Algorithm HMAC256(String secret) throws IllegalArgumentException { * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 256 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -152,7 +156,9 @@ public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { /** * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * - * @param secret the secret to use in the verify or signing instance. + * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 384 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -164,6 +170,8 @@ public static Algorithm HMAC384(String secret) throws IllegalArgumentException { * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 384 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -174,7 +182,9 @@ public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { /** * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". * - * @param secret the secret to use in the verify or signing instance. + * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 512 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -186,6 +196,8 @@ public static Algorithm HMAC512(String secret) throws IllegalArgumentException { * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 512 bit long + * See HMAC Key Length and Security in README * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ From 645b40a4c9ee9ed62e49d5bebda2c8a6c9d3221a Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Thu, 5 May 2022 11:54:49 +0200 Subject: [PATCH 201/293] Added test cases to differentiate thrown exception --- .../java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index a02c639b..44d9e6db 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -1370,6 +1370,7 @@ public void emptyECDSA256SignatureShouldFailTokenVerification() throws Exception @Test public void signatureWithAllZerosShouldFail() throws Exception { exception.expect(SignatureException.class); + exception.expectMessage("Invalid Signature: All Zeros."); ECPublicKey pubKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); @@ -1381,6 +1382,7 @@ public void signatureWithAllZerosShouldFail() throws Exception { @Test public void signatureWithRZeroShouldFail() throws Exception { exception.expect(SignatureException.class); + exception.expectMessage("Invalid Signature: All Zeros for R value."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1406,6 +1408,7 @@ public void signatureWithRZeroShouldFail() throws Exception { @Test public void signatureWithSZeroShouldFail() throws Exception { exception.expect(SignatureException.class); + exception.expectMessage("Invalid Signature: All Zeros for S value."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1431,6 +1434,7 @@ public void signatureWithSZeroShouldFail() throws Exception { @Test public void signatureWithRValueNotLessThanOrderShouldFail() throws Exception { exception.expect(SignatureException.class); + exception.expectMessage("The difference between R value and order should be greater than one."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1448,6 +1452,7 @@ public void signatureWithRValueNotLessThanOrderShouldFail() throws Exception { @Test public void signatureWithSValueNotLessThanOrderShouldFail() throws Exception { exception.expect(SignatureException.class); + exception.expectMessage("The difference between S value and order should be greater than one."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); From 9d471d2152354866bfa65a861dac5590601efe79 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Thu, 5 May 2022 14:10:52 +0200 Subject: [PATCH 202/293] Abstracted the error message thrown --- .../java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java | 10 +++++----- .../com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 00b68cad..ac34c44e 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -158,7 +158,7 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr } if (isAllZeros(joseSignature)) { - throw new SignatureException("Invalid Signature: All Zeros."); + throw new SignatureException("Invalid signature format."); } // get R @@ -166,7 +166,7 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr System.arraycopy(joseSignature, 0, rBytes, 0, ecNumberSize); BigInteger r = new BigInteger(1, rBytes); if(isAllZeros(rBytes)) { - throw new SignatureException("Invalid Signature: All Zeros for R value."); + throw new SignatureException("Invalid signature format."); } // get S @@ -174,7 +174,7 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr System.arraycopy(joseSignature, ecNumberSize, sBytes, 0, ecNumberSize); BigInteger s = new BigInteger(1, sBytes); if(isAllZeros(sBytes)) { - throw new SignatureException("Invalid Signature: All Zeros for S value."); + throw new SignatureException("Invalid signature format."); } //moved this check from JOSEToDER method @@ -192,11 +192,11 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr // R and S must be less than N if (order.compareTo(r) < 1) { - throw new SignatureException("The difference between R value and order should be greater than one."); + throw new SignatureException("Invalid signature format."); } if (order.compareTo(s) < 1){ - throw new SignatureException("The difference between S value and order should be greater than one."); + throw new SignatureException("Invalid signature format."); } } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 44d9e6db..818924fe 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -1370,7 +1370,7 @@ public void emptyECDSA256SignatureShouldFailTokenVerification() throws Exception @Test public void signatureWithAllZerosShouldFail() throws Exception { exception.expect(SignatureException.class); - exception.expectMessage("Invalid Signature: All Zeros."); + exception.expectMessage("Invalid signature format."); ECPublicKey pubKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); @@ -1382,7 +1382,7 @@ public void signatureWithAllZerosShouldFail() throws Exception { @Test public void signatureWithRZeroShouldFail() throws Exception { exception.expect(SignatureException.class); - exception.expectMessage("Invalid Signature: All Zeros for R value."); + exception.expectMessage("Invalid signature format."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1408,7 +1408,7 @@ public void signatureWithRZeroShouldFail() throws Exception { @Test public void signatureWithSZeroShouldFail() throws Exception { exception.expect(SignatureException.class); - exception.expectMessage("Invalid Signature: All Zeros for S value."); + exception.expectMessage("Invalid signature format."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1434,7 +1434,7 @@ public void signatureWithSZeroShouldFail() throws Exception { @Test public void signatureWithRValueNotLessThanOrderShouldFail() throws Exception { exception.expect(SignatureException.class); - exception.expectMessage("The difference between R value and order should be greater than one."); + exception.expectMessage("Invalid signature format."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); @@ -1452,7 +1452,7 @@ public void signatureWithRValueNotLessThanOrderShouldFail() throws Exception { @Test public void signatureWithSValueNotLessThanOrderShouldFail() throws Exception { exception.expect(SignatureException.class); - exception.expectMessage("The difference between S value and order should be greater than one."); + exception.expectMessage("Invalid signature format."); ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECPrivateKey privateKey = (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); From fd3eac3b721c7481a69d9109cc0666b65544ac5b Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Thu, 5 May 2022 17:17:28 +0200 Subject: [PATCH 203/293] Release 3.19.2 --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a022328d..d81f9a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [3.19.2](https://github.com/auth0/java-jwt/tree/3.19.2) (2022-05-05) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.1...3.19.2) + +**Security** +- [SDK-3311] Added protection against CVE-2022-21449 [\#579](https://github.com/auth0/java-jwt/pull/579) ([poovamraj](https://github.com/poovamraj)) + ## [3.19.1](https://github.com/auth0/java-jwt/tree/3.19.1) (2022-03-30) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.0...3.19.1) diff --git a/README.md b/README.md index 0e6a7d48..c55fa70c 100644 --- a/README.md +++ b/README.md @@ -25,14 +25,14 @@ The library is available on both Maven Central and Bintray, and the Javadoc is p com.auth0 java-jwt - 3.19.1 + 3.19.2 ``` ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.19.1' +implementation 'com.auth0:java-jwt:3.19.2' ``` ## Available Algorithms From 4573ba0537c5a34b768be4149ab817075cdbd111 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 6 May 2022 13:02:42 +0200 Subject: [PATCH 204/293] Code formatting for CheckStyle --- config/checkstyle/checkstyle.xml | 2 +- .../com/auth0/jwt/algorithms/ECDSAAlgorithm.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index d2615981..b97ccc37 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -206,7 +206,7 @@ value="Catch parameter name ''{0}'' must match pattern ''{1}''."/> - + diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java index 185d95cf..b3046097 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -50,7 +50,8 @@ public void verify(DecodedJWT jwt) throws SignatureVerificationException { throw new IllegalStateException("The given Public Key is null."); } validateSignatureStructure(signatureBytes, publicKey); - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); + boolean valid = crypto.verifySignatureFor( + getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); if (!valid) { throw new SignatureVerificationException(this); @@ -151,7 +152,7 @@ byte[] DERToJOSE(byte[] derSignature) throws SignatureException { * This method ensures the signature's structure is as expected. * * @param joseSignature is the signature from the JWT - * @param publicKey public key used to verify the JWT + * @param publicKey public key used to verify the JWT * @throws SignatureException if the signature's structure is not as per expectation */ // Visible for testing @@ -168,16 +169,14 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr // get R byte[] rBytes = new byte[ecNumberSize]; System.arraycopy(joseSignature, 0, rBytes, 0, ecNumberSize); - BigInteger r = new BigInteger(1, rBytes); - if(isAllZeros(rBytes)) { + if (isAllZeros(rBytes)) { throw new SignatureException("Invalid signature format."); } // get S byte[] sBytes = new byte[ecNumberSize]; System.arraycopy(joseSignature, ecNumberSize, sBytes, 0, ecNumberSize); - BigInteger s = new BigInteger(1, sBytes); - if(isAllZeros(sBytes)) { + if (isAllZeros(sBytes)) { throw new SignatureException("Invalid signature format."); } @@ -193,13 +192,15 @@ void validateSignatureStructure(byte[] joseSignature, ECPublicKey publicKey) thr } BigInteger order = publicKey.getParams().getOrder(); + BigInteger r = new BigInteger(1, rBytes); + BigInteger s = new BigInteger(1, sBytes); // R and S must be less than N if (order.compareTo(r) < 1) { throw new SignatureException("Invalid signature format."); } - if (order.compareTo(s) < 1){ + if (order.compareTo(s) < 1) { throw new SignatureException("Invalid signature format."); } } From 6f94d823682cf4ddb717d9e336c6f49004078b4b Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 6 May 2022 13:15:37 +0200 Subject: [PATCH 205/293] Removed ES256K tests since it is removed in v4 --- .../java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index d05fb53d..2e636c71 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -1223,15 +1223,12 @@ public void invalidECDSA256SignatureShouldFailTokenVerification() throws Excepti ECKey key256 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECKey key384 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); ECKey key512 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); - ECKey key256k = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); - JWTVerifier verifier256k = JWT.require(Algorithm.ECDSA256(key256k)).build(); verifier256.verify(jwtWithInvalidSig); verifier384.verify(jwtWithInvalidSig); verifier512.verify(jwtWithInvalidSig); - verifier256k.verify(jwtWithInvalidSig); } @Test @@ -1244,15 +1241,12 @@ public void emptyECDSA256SignatureShouldFailTokenVerification() throws Exception ECKey key256 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); ECKey key384 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); ECKey key512 = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); - ECKey key256k = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256K, "EC"); JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); - JWTVerifier verifier256k = JWT.require(Algorithm.ECDSA256(key256k)).build(); verifier256.verify(jwtWithInvalidSig); verifier384.verify(jwtWithInvalidSig); verifier512.verify(jwtWithInvalidSig); - verifier256k.verify(jwtWithInvalidSig); } @Test From c2e3558feb5cf6bd12f977b692236005053962fd Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 6 May 2022 14:46:45 +0200 Subject: [PATCH 206/293] Added tests to verify null creation in list and map --- .../java/com/auth0/jwt/JWTCreatorTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 4d1c7314..020e5e37 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -16,6 +16,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -919,4 +922,22 @@ public void withPayloadShouldSupportNullValuesEverywhere() { assertThat(headerJson, JsonMatcher.hasEntry("listNestedClaim", listNestedClaim)); assertThat(headerJson, JsonMatcher.hasEntry("objClaim", objClaim)); } + + @Test + public void shouldCreatePayloadWithNullForMap() { + String jwt = JWTCreator.init() + .withClaim("name", (Map) null) + .sign(Algorithm.HMAC256("secret")); + assertThat(jwt, is(notNullValue())); + assertTrue(JWT.decode(jwt).getClaim("name").isNull()); + } + + @Test + public void shouldCreatePayloadWithNullForList() { + String jwt = JWTCreator.init() + .withClaim("name", (List) null) + .sign(Algorithm.HMAC256("secret")); + assertThat(jwt, is(notNullValue())); + assertTrue(JWT.decode(jwt).getClaim("name").isNull()); + } } From 08f2e1de7ad86ad85a182c2d8a01ec04358be5ed Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 6 May 2022 16:01:51 +0200 Subject: [PATCH 207/293] Release 4.0.0-beta.0 --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d81f9a18..c920528c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Change Log +## [4.0.0-beta.0](https://github.com/auth0/java-jwt/tree/4.0.0-beta.0) (2022-05-06) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0...4.0.0-beta.0) + +**Added** +- [SDK-3159] JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3244] Add Migration Guide [\#576](https://github.com/auth0/java-jwt/pull/576) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3226] Expose claim name and header constants [\#574](https://github.com/auth0/java-jwt/pull/574) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3231] Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) +- Improved README structure [\#571](https://github.com/auth0/java-jwt/pull/571) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3154] Improved Exception Handling [\#568](https://github.com/auth0/java-jwt/pull/568) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3155] Predicate based Claim verification [\#562](https://github.com/auth0/java-jwt/pull/562) ([poovamraj](https://github.com/poovamraj)) +- Add lint checks [\#561](https://github.com/auth0/java-jwt/pull/561) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3186] Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3149] Add Instant support [\#537](https://github.com/auth0/java-jwt/pull/537) ([jimmyjames](https://github.com/jimmyjames)) +- Testing Java LTS versions [\#536](https://github.com/auth0/java-jwt/pull/536) ([poovamraj](https://github.com/poovamraj)) + +**Changed** +- [SDK-3158] Null claim handling [\#564](https://github.com/auth0/java-jwt/pull/564) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3151] Undeprecate Single Key Constructor for Algorithms [\#551](https://github.com/auth0/java-jwt/pull/551) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3151] Update documentation and undeprecate single content sign methods [\#550](https://github.com/auth0/java-jwt/pull/550) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3197] Update test deps [\#539](https://github.com/auth0/java-jwt/pull/539) ([jimmyjames](https://github.com/jimmyjames)) + +**Deprecated** +- [SDK-3192] Deprecate secp256k1 curve for EC Algorithms [\#540](https://github.com/auth0/java-jwt/pull/540) ([poovamraj](https://github.com/poovamraj)) + +**Removed** +- Remove ES256K support [\#556](https://github.com/auth0/java-jwt/pull/556) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3212] Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3160] Remove internal Clock [\#533](https://github.com/auth0/java-jwt/pull/533) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Improve keyprovider reliability [\#570](https://github.com/auth0/java-jwt/pull/570) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3186] Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) +- Test only change - remove unnecessary throws clause from tests [\#535](https://github.com/auth0/java-jwt/pull/535) ([jimmyjames](https://github.com/jimmyjames)) + +**Security** +- [SDK-3125] Updated documentation regarding HMAC Key length [\#580](https://github.com/auth0/java-jwt/pull/580) ([poovamraj](https://github.com/poovamraj)) + +**Breaking changes** +- [SDK-3231] Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) +- Improve keyprovider reliability [\#570](https://github.com/auth0/java-jwt/pull/570) ([poovamraj](https://github.com/poovamraj)) +- Remove ES256K support [\#556](https://github.com/auth0/java-jwt/pull/556) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3212] Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) +- [SDK-3171] Fix header claims serialization [\#549](https://github.com/auth0/java-jwt/pull/549) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3150] Serialize dates in collections as seconds since epoch [\#534](https://github.com/auth0/java-jwt/pull/534) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3160] Replace com.auth0.jwt.interfaces.Clock with java.time.Clock [\#532](https://github.com/auth0/java-jwt/pull/532) ([jimmyjames](https://github.com/jimmyjames)) + ## [3.19.2](https://github.com/auth0/java-jwt/tree/3.19.2) (2022-05-05) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.1...3.19.2) From a14643d4a9cca096d3dc2049db6a4e0522d454d9 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 6 May 2022 16:10:37 +0200 Subject: [PATCH 208/293] Updated Changelog for beta --- CHANGELOG.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c920528c..123788f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,51 +1,53 @@ # Change Log ## [4.0.0-beta.0](https://github.com/auth0/java-jwt/tree/4.0.0-beta.0) (2022-05-06) -[Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0...4.0.0-beta.0) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0-beta.0) + +💡 Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. **Added** -- [SDK-3159] JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3244] Add Migration Guide [\#576](https://github.com/auth0/java-jwt/pull/576) ([jimmyjames](https://github.com/jimmyjames)) -- [SDK-3226] Expose claim name and header constants [\#574](https://github.com/auth0/java-jwt/pull/574) ([jimmyjames](https://github.com/jimmyjames)) -- [SDK-3231] Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) +- JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) +- Add Migration Guide [\#576](https://github.com/auth0/java-jwt/pull/576) ([jimmyjames](https://github.com/jimmyjames)) +- Expose claim name and header constants [\#574](https://github.com/auth0/java-jwt/pull/574) ([jimmyjames](https://github.com/jimmyjames)) +- Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) - Improved README structure [\#571](https://github.com/auth0/java-jwt/pull/571) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3154] Improved Exception Handling [\#568](https://github.com/auth0/java-jwt/pull/568) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3155] Predicate based Claim verification [\#562](https://github.com/auth0/java-jwt/pull/562) ([poovamraj](https://github.com/poovamraj)) +- Improved Exception Handling [\#568](https://github.com/auth0/java-jwt/pull/568) ([poovamraj](https://github.com/poovamraj)) +- Predicate based Claim verification [\#562](https://github.com/auth0/java-jwt/pull/562) ([poovamraj](https://github.com/poovamraj)) - Add lint checks [\#561](https://github.com/auth0/java-jwt/pull/561) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3186] Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) -- [SDK-3149] Add Instant support [\#537](https://github.com/auth0/java-jwt/pull/537) ([jimmyjames](https://github.com/jimmyjames)) +- Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) +- Add Instant support [\#537](https://github.com/auth0/java-jwt/pull/537) ([jimmyjames](https://github.com/jimmyjames)) - Testing Java LTS versions [\#536](https://github.com/auth0/java-jwt/pull/536) ([poovamraj](https://github.com/poovamraj)) **Changed** -- [SDK-3158] Null claim handling [\#564](https://github.com/auth0/java-jwt/pull/564) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3151] Undeprecate Single Key Constructor for Algorithms [\#551](https://github.com/auth0/java-jwt/pull/551) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3151] Update documentation and undeprecate single content sign methods [\#550](https://github.com/auth0/java-jwt/pull/550) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3197] Update test deps [\#539](https://github.com/auth0/java-jwt/pull/539) ([jimmyjames](https://github.com/jimmyjames)) +- Null claim handling [\#564](https://github.com/auth0/java-jwt/pull/564) ([poovamraj](https://github.com/poovamraj)) +- Undeprecate Single Key Constructor for Algorithms [\#551](https://github.com/auth0/java-jwt/pull/551) ([poovamraj](https://github.com/poovamraj)) +- Update documentation and undeprecate single content sign methods [\#550](https://github.com/auth0/java-jwt/pull/550) ([poovamraj](https://github.com/poovamraj)) +- Update test deps [\#539](https://github.com/auth0/java-jwt/pull/539) ([jimmyjames](https://github.com/jimmyjames)) **Deprecated** -- [SDK-3192] Deprecate secp256k1 curve for EC Algorithms [\#540](https://github.com/auth0/java-jwt/pull/540) ([poovamraj](https://github.com/poovamraj)) +- Deprecate secp256k1 curve for EC Algorithms [\#540](https://github.com/auth0/java-jwt/pull/540) ([poovamraj](https://github.com/poovamraj)) **Removed** - Remove ES256K support [\#556](https://github.com/auth0/java-jwt/pull/556) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3212] Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3160] Remove internal Clock [\#533](https://github.com/auth0/java-jwt/pull/533) ([jimmyjames](https://github.com/jimmyjames)) +- Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) +- Remove internal Clock [\#533](https://github.com/auth0/java-jwt/pull/533) ([jimmyjames](https://github.com/jimmyjames)) **Fixed** - Improve keyprovider reliability [\#570](https://github.com/auth0/java-jwt/pull/570) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3186] Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) +- Support date/time custom claim validation [\#538](https://github.com/auth0/java-jwt/pull/538) ([jimmyjames](https://github.com/jimmyjames)) - Test only change - remove unnecessary throws clause from tests [\#535](https://github.com/auth0/java-jwt/pull/535) ([jimmyjames](https://github.com/jimmyjames)) **Security** -- [SDK-3125] Updated documentation regarding HMAC Key length [\#580](https://github.com/auth0/java-jwt/pull/580) ([poovamraj](https://github.com/poovamraj)) +- Updated documentation regarding HMAC Key length [\#580](https://github.com/auth0/java-jwt/pull/580) ([poovamraj](https://github.com/poovamraj)) **Breaking changes** -- [SDK-3231] Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) +- Added support for multiple checks on a single claim [\#573](https://github.com/auth0/java-jwt/pull/573) ([poovamraj](https://github.com/poovamraj)) - Improve keyprovider reliability [\#570](https://github.com/auth0/java-jwt/pull/570) ([poovamraj](https://github.com/poovamraj)) - Remove ES256K support [\#556](https://github.com/auth0/java-jwt/pull/556) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3212] Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) -- [SDK-3171] Fix header claims serialization [\#549](https://github.com/auth0/java-jwt/pull/549) ([jimmyjames](https://github.com/jimmyjames)) -- [SDK-3150] Serialize dates in collections as seconds since epoch [\#534](https://github.com/auth0/java-jwt/pull/534) ([jimmyjames](https://github.com/jimmyjames)) -- [SDK-3160] Replace com.auth0.jwt.interfaces.Clock with java.time.Clock [\#532](https://github.com/auth0/java-jwt/pull/532) ([jimmyjames](https://github.com/jimmyjames)) +- Remove impl package export in module-info [\#553](https://github.com/auth0/java-jwt/pull/553) ([poovamraj](https://github.com/poovamraj)) +- Fix header claims serialization [\#549](https://github.com/auth0/java-jwt/pull/549) ([jimmyjames](https://github.com/jimmyjames)) +- Serialize dates in collections as seconds since epoch [\#534](https://github.com/auth0/java-jwt/pull/534) ([jimmyjames](https://github.com/jimmyjames)) +- Replace com.auth0.jwt.interfaces.Clock with java.time.Clock [\#532](https://github.com/auth0/java-jwt/pull/532) ([jimmyjames](https://github.com/jimmyjames)) ## [3.19.2](https://github.com/auth0/java-jwt/tree/3.19.2) (2022-05-05) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.1...3.19.2) From 90a68cbeba433c1634450f7d4d32e882aec5bd79 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 24 Jun 2022 11:59:21 +0200 Subject: [PATCH 209/293] Release 4.0.0 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123788f0..6f330308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ # Change Log +## [4.0.0](https://github.com/auth0/java-jwt/tree/4.0.0) (2022-06-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0-beta.0...4.0.0) + ## [4.0.0-beta.0](https://github.com/auth0/java-jwt/tree/4.0.0-beta.0) (2022-05-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0-beta.0) -💡 Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. +? Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. **Added** - JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) From b786c9dd42c2332c3caaca3b37198e8025c4b912 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 24 Jun 2022 12:16:27 +0200 Subject: [PATCH 210/293] Update release note in CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f330308..57edfa51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,24 @@ # Change Log ## [4.0.0](https://github.com/auth0/java-jwt/tree/4.0.0) (2022-06-24) -[Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0-beta.0...4.0.0) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0) + +**This is a major release and contains breaking changes!** + +- Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. + +### Main features +- Predicates based claim verification +- Support for Instant API and Lambda functions +- Improved Exceptions API +- Consistent null handling + +See the changelog entries for additional details. ## [4.0.0-beta.0](https://github.com/auth0/java-jwt/tree/4.0.0-beta.0) (2022-05-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0-beta.0) -? Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. +- Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. **Added** - JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) From 84dc66063790905561e3bb2addffcd5bc7125697 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 24 Jun 2022 12:19:12 +0200 Subject: [PATCH 211/293] Fix emoji used in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57edfa51..6023d836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ See the changelog entries for additional details. ## [4.0.0-beta.0](https://github.com/auth0/java-jwt/tree/4.0.0-beta.0) (2022-05-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0-beta.0) -- Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. +💡 Check the [Migration Guide](https://github.com/auth0/java-jwt/blob/master/MIGRATION_GUIDE.md) to understand the changes required to migrate your application to v4. **Added** - JavaDoc updated [\#577](https://github.com/auth0/java-jwt/pull/577) ([poovamraj](https://github.com/poovamraj)) From 929b99ed71c086b044e9f38fb6996298b604d58a Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 24 Jun 2022 13:22:48 +0200 Subject: [PATCH 212/293] Update README.md --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f649a742..77fb30fc 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This library is supported for Java 8, 11, and 17. For issues on non-LTS versions ### Gradle ```gradle -implementation 'com.auth0:java-jwt:3.19.0' +implementation 'com.auth0:java-jwt:4.0.0' ``` ### Maven @@ -48,16 +48,10 @@ implementation 'com.auth0:java-jwt:3.19.0' com.auth0 java-jwt - 3.19.2 + 4.0.0 ``` -### Gradle - -```gradle -implementation 'com.auth0:java-jwt:3.19.2' -``` - ## Available Algorithms The library implements JWT Verification and Signing using the following algorithms: From 3e6f4b80a125df67a97df6d8eadf675d79907624 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Tue, 5 Jul 2022 17:12:01 -0400 Subject: [PATCH 213/293] Create semgrep.yml --- .github/workflows/semgrep.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/semgrep.yml diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..e0227e37 --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,24 @@ +name: Semgrep + +on: + pull_request: {} + + push: + branches: ["master", "main"] + + schedule: + - cron: '30 0 1,15 * *' + +jobs: + semgrep: + name: Scan + runs-on: ubuntu-latest + container: + image: returntocorp/semgrep + if: (github.actor != 'dependabot[bot]') + steps: + - uses: actions/checkout@v3 + + - run: semgrep ci + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} From 190132ca282c2b6ff2d12b04c1ea07224ad9f271 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Fri, 8 Jul 2022 00:24:46 -0500 Subject: [PATCH 214/293] Update config.yml --- .circleci/config.yml | 48 +++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f47d6efa..c6425bb8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,6 @@ version: 2.1 +orbs: + codecov: codecov/codecov@3 commands: checkout-and-build: @@ -8,9 +10,9 @@ commands: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- - run: ./gradlew clean build - save_cache: paths: @@ -19,10 +21,7 @@ commands: run-tests: steps: - run: ./gradlew check jacocoTestReport --continue --console=plain - - run: - name: Upload Coverage - when: on_success - command: bash <(curl -s https://codecov.io/bash) -Z -C $CIRCLE_SHA1 + - codecov/upload # TODO re-enable once 4.0.0 is released # run-api-diff: # steps: @@ -43,36 +42,21 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb -# api-diff: -# docker: -# - image: openjdk:11.0-jdk -# steps: -# - checkout-and-build -# - run-api-diff -# environment: -# GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' -# _JAVA_OPTIONS: "-Xms512m -Xmx1024m" -# TERM: dumb - semgrep: - docker: - - image: returntocorp/semgrep-agent:v1 - environment: - SEMGREP_REPO_NAME: "auth0/java-jwt" - SEMGREP_REPO_URL: "https://github.com/auth0/java-jwt" - steps: - - checkout - - run: - name: Run vulnerabilities tests (Semgrep) - command: | - semgrep-agent --baseline-ref master --publish-token $SEMGREP_TOKEN + # api-diff: + # docker: + # - image: openjdk:11.0-jdk + # steps: + # - checkout-and-build + # - run-api-diff + # environment: + # GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + # _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + # TERM: dumb workflows: build-and-test: jobs: - build - - semgrep: - context: - - semgrep-env # api-diff: # jobs: # - api-diff From 8dc5fbb6cde11084c9c74096d39f869910733bb8 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Mon, 11 Jul 2022 15:29:08 +0200 Subject: [PATCH 215/293] reenable api-diff check for 4.0.0 --- .circleci/config.yml | 43 +++++++++++++++++++++---------------------- lib/build.gradle | 2 +- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c6425bb8..8532ec75 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,15 +22,14 @@ commands: steps: - run: ./gradlew check jacocoTestReport --continue --console=plain - codecov/upload -# TODO re-enable once 4.0.0 is released -# run-api-diff: -# steps: -# # run apiDiff task -# - run: ./gradlew apiDiff -# - store_artifacts: -# path: lib/build/reports/apiDiff/apiDiff.txt -# - store_artifacts: -# path: lib/build/reports/apiDiff/apiDiff.html + run-api-diff: + steps: + # run apiDiff task + - run: ./gradlew apiDiff + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.txt + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.html jobs: build: docker: @@ -42,21 +41,21 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb - # api-diff: - # docker: - # - image: openjdk:11.0-jdk - # steps: - # - checkout-and-build - # - run-api-diff - # environment: - # GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - # _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - # TERM: dumb + api-diff: + docker: + - image: openjdk:11.0-jdk + steps: + - checkout-and-build + - run-api-diff + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + TERM: dumb workflows: build-and-test: jobs: - build -# api-diff: -# jobs: -# - api-diff + api-diff: + jobs: + - api-diff diff --git a/lib/build.gradle b/lib/build.gradle index b1b24889..01a5adbe 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -17,7 +17,7 @@ oss { repository "java-jwt" organization "auth0" description "Java implementation of JSON Web Token (JWT)" - baselineCompareVersion "3.18.2" + baselineCompareVersion "4.0.0" developers { auth0 { From f30a47b917478010aa91a2093f9db786ef9a7e32 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Mon, 11 Jul 2022 15:44:38 +0200 Subject: [PATCH 216/293] Update config.yml --- .circleci/config.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8532ec75..1675c39f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,16 +41,16 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb - api-diff: - docker: - - image: openjdk:11.0-jdk - steps: - - checkout-and-build - - run-api-diff - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb + api-diff: + docker: + - image: openjdk:11.0-jdk + steps: + - checkout-and-build + - run-api-diff + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + TERM: dumb workflows: build-and-test: From 0bf73a60152ff86014d6687c336851fe78190aba Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Fri, 22 Jul 2022 10:59:13 +0200 Subject: [PATCH 217/293] Provide straightforward example for JWKS --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 77fb30fc..822e9db7 100644 --- a/README.md +++ b/README.md @@ -199,10 +199,10 @@ By using a `KeyProvider` you can change in runtime the key used either to verify - `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. -The following example shows how this would work with `JwkStore`, an imaginary [JWK Set](https://auth0.com/docs/jwks) implementation. For simple key rotation using JWKS, try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. +The following example shows how this would work with `JwkProvider` from the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. ```java -final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}"); +final JwkProvider jwkStore = new UrlJwkProvider("https://samples.auth0.com/"); final RSAPrivateKey privateKey = //Get the key instance final String privateKeyId = //Create an Id for the above key @@ -210,7 +210,7 @@ RSAKeyProvider keyProvider = new RSAKeyProvider() { @Override public RSAPublicKey getPublicKeyById(String kid) { //Received 'kid' value might be null if it wasn't defined in the Token's header - RSAPublicKey publicKey = jwkStore.get(kid); + PublicKey publicKey = jwkStore.get(kid).getPublicKey(); return (RSAPublicKey) publicKey; } From 81c8b46b8974bd2160218c2b5116e6d4dfae4a61 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Mon, 25 Jul 2022 11:02:43 +0200 Subject: [PATCH 218/293] Updated variable name for README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 822e9db7..bbc166c1 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ By using a `KeyProvider` you can change in runtime the key used either to verify The following example shows how this would work with `JwkProvider` from the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. ```java -final JwkProvider jwkStore = new UrlJwkProvider("https://samples.auth0.com/"); +final JwkProvider jwkProvider = new UrlJwkProvider("https://samples.auth0.com/"); final RSAPrivateKey privateKey = //Get the key instance final String privateKeyId = //Create an Id for the above key @@ -210,7 +210,7 @@ RSAKeyProvider keyProvider = new RSAKeyProvider() { @Override public RSAPublicKey getPublicKeyById(String kid) { //Received 'kid' value might be null if it wasn't defined in the Token's header - PublicKey publicKey = jwkStore.get(kid).getPublicKey(); + PublicKey publicKey = jwkProvider.get(kid).getPublicKey(); return (RSAPublicKey) publicKey; } From c36ec6ff80c0d62668a5ae726f9198d3ca587a17 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Mon, 22 Aug 2022 10:51:52 +0200 Subject: [PATCH 219/293] Make JWT constants final values --- lib/src/main/java/com/auth0/jwt/HeaderParams.java | 8 ++++---- .../main/java/com/auth0/jwt/RegisteredClaims.java | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/HeaderParams.java b/lib/src/main/java/com/auth0/jwt/HeaderParams.java index 7af3c442..1107f313 100644 --- a/lib/src/main/java/com/auth0/jwt/HeaderParams.java +++ b/lib/src/main/java/com/auth0/jwt/HeaderParams.java @@ -10,20 +10,20 @@ private HeaderParams() {} /** * The algorithm used to sign a JWT. */ - public static String ALGORITHM = "alg"; + public static final String ALGORITHM = "alg"; /** * The content type of the JWT. */ - public static String CONTENT_TYPE = "cty"; + public static final String CONTENT_TYPE = "cty"; /** * The media type of the JWT. */ - public static String TYPE = "typ"; + public static final String TYPE = "typ"; /** * The key ID of a JWT used to specify the key for signature validation. */ - public static String KEY_ID = "kid"; + public static final String KEY_ID = "kid"; } diff --git a/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java index 8c47eb51..c5509716 100644 --- a/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java +++ b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java @@ -13,43 +13,43 @@ private RegisteredClaims() { * The "iss" (issuer) claim identifies the principal that issued the JWT. * Refer RFC 7529 Section 4.1.1 */ - public static String ISSUER = "iss"; + public static final String ISSUER = "iss"; /** * The "sub" (subject) claim identifies the principal that is the subject of the JWT. * Refer RFC 7529 Section 4.1.2 */ - public static String SUBJECT = "sub"; + public static final String SUBJECT = "sub"; /** * The "aud" (audience) claim identifies the recipients that the JWT is intended for. * Refer RFC 7529 Section 4.1.3 */ - public static String AUDIENCE = "aud"; + public static final String AUDIENCE = "aud"; /** * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be * accepted for processing. * Refer RFC 7529 Section 4.1.4 */ - public static String EXPIRES_AT = "exp"; + public static final String EXPIRES_AT = "exp"; /** * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. * Refer RFC 7529 Section 4.1.5 */ - public static String NOT_BEFORE = "nbf"; + public static final String NOT_BEFORE = "nbf"; /** * The "iat" (issued at) claim identifies the time at which the JWT was issued. * Refer RFC 7529 Section 4.1.6 */ - public static String ISSUED_AT = "iat"; + public static final String ISSUED_AT = "iat"; /** * The "jti" (JWT ID) claim provides a unique identifier for the JWT. * Refer RFC 7529 Section 4.1.7 */ - public static String JWT_ID = "jti"; + public static final String JWT_ID = "jti"; } From 15202989d733ce695c302d2f2029bd67c919fd99 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 29 Aug 2022 19:32:23 -0500 Subject: [PATCH 220/293] [SDK-3816] Update docs for verification thread-safety --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 6 +++++- .../java/com/auth0/jwt/interfaces/JWTVerifier.java | 14 +++++++++++++- .../com/auth0/jwt/interfaces/Verification.java | 4 +++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 0bc17fb1..07c86a4c 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -46,7 +46,11 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { } /** - * {@link Verification} implementation that accepts all the expected Claim values for verification. + * {@link Verification} implementation that accepts all the expected Claim values for verification, and + * builds a {@link com.auth0.jwt.interfaces.JWTVerifier} used to verify a JWT's signature and expected claims. + * + * Note that this class is not thread-safe. Calling {@link #build()} returns an instance of + * {@link com.auth0.jwt.interfaces.JWTVerifier} which can be reused. */ public static class BaseVerification implements Verification { private final Algorithm algorithm; diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java index b7030a97..2756ddd8 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -4,7 +4,19 @@ /** - * Used to verify the JWT for its signature and claims. + * Used to verify the JWT for its signature and claims. Implementations must be thread-safe. Instances are created + * using {@link Verification}. + * + *
+ * try {
+ *      JWTVerifier verifier = JWTVerifier.init(Algorithm.RSA256(publicKey, privateKey)
+ *          .withIssuer("auth0")
+ *          .build();
+ *      DecodedJWT jwt = verifier.verify("token");
+ * } catch (JWTVerificationException e) {
+ *      // invalid signature or claims
+ * }
+ * 
*/ public interface JWTVerifier { diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java index 4a8a0f84..b4adcf5c 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -7,7 +7,9 @@ import java.util.function.BiPredicate; /** - * Constructs and holds the checks required for a JWT to be considered valid. + * Constructs and holds the checks required for a JWT to be considered valid. Note that implementations are + * not thread-safe. Once built by calling {@link #build()}, the resulting + * {@link com.auth0.jwt.interfaces.JWTVerifier} is thread-safe. */ public interface Verification { From f530b268354926792461926a0eb8d810be01583b Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 29 Aug 2022 20:11:33 -0500 Subject: [PATCH 221/293] Check for null token before splitting --- lib/src/main/java/com/auth0/jwt/TokenUtils.java | 3 +++ lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/lib/src/main/java/com/auth0/jwt/TokenUtils.java b/lib/src/main/java/com/auth0/jwt/TokenUtils.java index 0a76028b..6fa731c5 100644 --- a/lib/src/main/java/com/auth0/jwt/TokenUtils.java +++ b/lib/src/main/java/com/auth0/jwt/TokenUtils.java @@ -12,6 +12,9 @@ abstract class TokenUtils { * @throws JWTDecodeException if the Token doesn't have 3 parts. */ static String[] splitToken(String token) throws JWTDecodeException { + if (token == null) { + throw new JWTDecodeException("The token is null."); + } String[] parts = token.split("\\."); if (parts.length == 2 && token.endsWith(".")) { //Tokens with alg='none' have empty String as Signature. diff --git a/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java index d32ee859..8649ec90 100644 --- a/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java +++ b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java @@ -52,4 +52,11 @@ public void shouldThrowOnSplitTokenWithLessThan3Parts() { String token = "two.parts"; TokenUtils.splitToken(token); } + + @Test + public void shouldThrowOnSplitTokenWithNullValue() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The token is null."); + TokenUtils.splitToken(null); + } } \ No newline at end of file From 39eec490bd9ad07f7260e9fa1fd0cf60369c6036 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 31 Aug 2022 15:34:11 +0200 Subject: [PATCH 222/293] Trigger Build From 122a8af5c09b72a6fd22185970031c1a5b3e6961 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 31 Aug 2022 15:52:11 +0200 Subject: [PATCH 223/293] Disable API diff task temporarily --- .circleci/config.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1675c39f..fb185917 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,14 +22,14 @@ commands: steps: - run: ./gradlew check jacocoTestReport --continue --console=plain - codecov/upload - run-api-diff: - steps: - # run apiDiff task - - run: ./gradlew apiDiff - - store_artifacts: - path: lib/build/reports/apiDiff/apiDiff.txt - - store_artifacts: - path: lib/build/reports/apiDiff/apiDiff.html + # run-api-diff: + # steps: + # # run apiDiff task + # - run: ./gradlew apiDiff + # - store_artifacts: + # path: lib/build/reports/apiDiff/apiDiff.txt + # - store_artifacts: + # path: lib/build/reports/apiDiff/apiDiff.html jobs: build: docker: From 89fbc61318641b88e3b8b400ff4e8a67d04632fa Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 31 Aug 2022 15:56:09 +0200 Subject: [PATCH 224/293] Disable API diff --- .circleci/config.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fb185917..73f58ddb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -41,21 +41,21 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb - api-diff: - docker: - - image: openjdk:11.0-jdk - steps: - - checkout-and-build - - run-api-diff - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb + # api-diff: + # docker: + # - image: openjdk:11.0-jdk + # steps: + # - checkout-and-build + # - run-api-diff + # environment: + # GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + # _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + # TERM: dumb workflows: build-and-test: jobs: - build - api-diff: - jobs: - - api-diff + # api-diff: + # jobs: + # - api-diff From 884ac926d7f7afb7e42036a65dc739e9bed79e25 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 7 Sep 2022 10:22:13 -0500 Subject: [PATCH 225/293] Update OSS plugin to latest --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index b614415b..a4cda2d7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,7 +3,7 @@ pluginManagement { gradlePluginPortal() } plugins { - id 'com.auth0.gradle.oss-library.java' version '0.16.0' + id 'com.auth0.gradle.oss-library.java' version '0.17.2' } } From f29d70c56768bd7ed8356a44040082087e076cd8 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 8 Sep 2022 21:41:03 -0500 Subject: [PATCH 226/293] Update to gradle 6.9.2 --- gradle/wrapper/gradle-wrapper.properties | 2 +- lib/build.gradle | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4d9ca164..ec991f9a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/build.gradle b/lib/build.gradle index 01a5adbe..eab6a0a5 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -44,7 +44,7 @@ java { compileJava { exclude 'module-info.java' // Required to be compatible with JDK 8+ - options.compilerArgs = ['--release', "8"] + options.release = 8 } javadoc { @@ -99,7 +99,8 @@ task compileModuleInfoJava(type: JavaCompile) { } compileTestJava { - options.compilerArgs = ['--release', "8", "-Xlint:deprecation"] + options.release = 8 + options.compilerArgs = ["-Xlint:deprecation"] } def testJava8 = tasks.register('testJava8', Test) { From adbabeb7c45fc3862a5d5b2c64321d06a33c725d Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 19 Sep 2022 08:17:15 -0500 Subject: [PATCH 227/293] Add Ship CLI support --- .shiprc | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .shiprc diff --git a/.shiprc b/.shiprc new file mode 100644 index 00000000..7509d078 --- /dev/null +++ b/.shiprc @@ -0,0 +1,6 @@ +{ + "files": { + "README.md": [] + }, + "prefixVersion": false +} \ No newline at end of file From 75a0bc0f9f0411891bb0e791e13dfa91a01e38ac Mon Sep 17 00:00:00 2001 From: frederikprijck Date: Wed, 5 Oct 2022 14:52:37 +0200 Subject: [PATCH 228/293] Add integration with our Shipping orb --- .circleci/config.yml | 12 ++++++++++++ .shiprc | 3 ++- lib/build.gradle | 19 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 73f58ddb..4741868d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,6 @@ version: 2.1 orbs: + ship: auth0/ship@0.7.1 codecov: codecov/codecov@3 commands: @@ -56,6 +57,17 @@ workflows: build-and-test: jobs: - build + - ship/java-publish: + prefix-tag: false + context: + - publish-gh + - publish-sonatype + filters: + branches: + only: + - master + requires: + - build # api-diff: # jobs: # - api-diff diff --git a/.shiprc b/.shiprc index 7509d078..a00fb91e 100644 --- a/.shiprc +++ b/.shiprc @@ -1,6 +1,7 @@ { "files": { - "README.md": [] + "README.md": [], + "lib/build.gradle": [] }, "prefixVersion": false } \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index eab6a0a5..74cdb5c1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,3 +1,7 @@ +buildscript { + version = "4.0.0" +} + plugins { id 'java' id 'jacoco' @@ -5,6 +9,13 @@ plugins { id 'checkstyle' } +def signingKey = findProperty('SIGNING_KEY') +def signingKeyPwd = findProperty('SIGNING_PASSWORD') + +signing { + useInMemoryPgpKeys(signingKey, signingKeyPwd) +} + checkstyle { toolVersion '10.0' checkstyleTest.enabled = false //We are disabling lint checks for tests @@ -134,3 +145,11 @@ jar { compileModuleInfoJava.dependsOn compileJava classes.dependsOn compileModuleInfoJava + +// Creates a version.txt file containing the current version of the SDK. +// This file is picked up and parsed by our Ship Orb to determine the version. +task exportVersion() { + doLast { + new File(rootDir, "version.txt").text = "$version" + } +} From d2bf8d35e96ea8233f2a80ca6d7371ad54e5ba18 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 6 Oct 2022 13:47:56 -0500 Subject: [PATCH 229/293] Release 4.1.0 --- CHANGELOG.md | 21 +++++++++++++++++++++ README.md | 4 ++-- lib/build.gradle | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6023d836..04796fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## [4.1.0](https://github.com/auth0/java-jwt/tree/4.1.0) (2022-10-06) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0...4.1.0) + +**⚠️ BREAKING CHANGES** +- Make JWT constants final values [\#604](https://github.com/auth0/java-jwt/pull/604) ([poovamraj](https://github.com/poovamraj)) + +**Added** +- Add integration with our Shipping orb [\#612](https://github.com/auth0/java-jwt/pull/612) ([frederikprijck](https://github.com/frederikprijck)) +- Add Ship CLI support [\#609](https://github.com/auth0/java-jwt/pull/609) ([jimmyjames](https://github.com/jimmyjames)) +- Provide straightforward example for JWKS [\#600](https://github.com/auth0/java-jwt/pull/600) ([poovamraj](https://github.com/poovamraj)) + +**Changed** +- Update to gradle 6.9.2 [\#608](https://github.com/auth0/java-jwt/pull/608) ([jimmyjames](https://github.com/jimmyjames)) +- Update OSS plugin to latest [\#607](https://github.com/auth0/java-jwt/pull/607) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3466] Upgrade Codecov [\#595](https://github.com/auth0/java-jwt/pull/595) ([evansims](https://github.com/evansims)) +- Update README.md [\#590](https://github.com/auth0/java-jwt/pull/590) ([poovamraj](https://github.com/poovamraj)) + +**Fixed** +- Check for null token before splitting [\#606](https://github.com/auth0/java-jwt/pull/606) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-3816] Update docs for verification thread-safety [\#605](https://github.com/auth0/java-jwt/pull/605) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.0.0](https://github.com/auth0/java-jwt/tree/4.0.0) (2022-06-24) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.19.2...4.0.0) diff --git a/README.md b/README.md index bbc166c1..a4fa509e 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This library is supported for Java 8, 11, and 17. For issues on non-LTS versions ### Gradle ```gradle -implementation 'com.auth0:java-jwt:4.0.0' +implementation 'com.auth0:java-jwt:4.1.0' ``` ### Maven @@ -48,7 +48,7 @@ implementation 'com.auth0:java-jwt:4.0.0' com.auth0 java-jwt - 4.0.0 + 4.1.0 ``` diff --git a/lib/build.gradle b/lib/build.gradle index 74cdb5c1..8ec1b630 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,5 @@ buildscript { - version = "4.0.0" + version = "4.1.0" } plugins { From 9c708c2ca0a9c7a6ab7b900fcf4292d0654a78ea Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Mon, 10 Oct 2022 21:08:49 +0200 Subject: [PATCH 230/293] Update config.yml --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4741868d..505c6f70 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@0.7.1 + ship: auth0/ship@dev:a88c4d7 codecov: codecov/codecov@3 commands: @@ -66,6 +66,7 @@ workflows: branches: only: - master + - automated-release requires: - build # api-diff: From 7cac89a50bafa65cafebbc919d8b18616b173853 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Mon, 10 Oct 2022 21:19:03 +0200 Subject: [PATCH 231/293] Update config.yml --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 505c6f70..b431f8d7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -59,6 +59,7 @@ workflows: - build - ship/java-publish: prefix-tag: false + package-name: java-jwt context: - publish-gh - publish-sonatype From 245c019bd20bb479b68f220d576e5ad78cae9779 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Mon, 10 Oct 2022 21:23:39 +0200 Subject: [PATCH 232/293] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b431f8d7..10a12231 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@dev:a88c4d7 + ship: auth0/ship@dev:9412fdc codecov: codecov/codecov@3 commands: From bd9c8133025c9da93ce7d5f8885fea43b75c2421 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Mon, 10 Oct 2022 21:35:27 +0200 Subject: [PATCH 233/293] Update config.yml --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 10a12231..6a313263 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@dev:9412fdc + ship: auth0/ship@dev:d4bf2df codecov: codecov/codecov@3 commands: @@ -59,7 +59,6 @@ workflows: - build - ship/java-publish: prefix-tag: false - package-name: java-jwt context: - publish-gh - publish-sonatype From 6a666b0c822d8a453405e67f702c0b326d2d8a5c Mon Sep 17 00:00:00 2001 From: frederikprijck Date: Tue, 11 Oct 2022 12:05:04 +0200 Subject: [PATCH 234/293] update build.gradle file --- lib/build.gradle | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 8ec1b630..d7951ff3 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -9,8 +9,8 @@ plugins { id 'checkstyle' } -def signingKey = findProperty('SIGNING_KEY') -def signingKeyPwd = findProperty('SIGNING_PASSWORD') +def signingKey = findProperty('signingKey') +def signingKeyPwd = findProperty('signingPassword') signing { useInMemoryPgpKeys(signingKey, signingKeyPwd) @@ -29,6 +29,7 @@ oss { organization "auth0" description "Java implementation of JSON Web Token (JWT)" baselineCompareVersion "4.0.0" + skipAssertSigningConfiguration true developers { auth0 { @@ -66,6 +67,7 @@ javadoc { dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' testImplementation 'net.jodah:concurrentunit:0.4.6' From 4fbe13f8da785c4826cf5b8fd4c3fec708cdda89 Mon Sep 17 00:00:00 2001 From: frederikprijck Date: Tue, 11 Oct 2022 12:18:20 +0200 Subject: [PATCH 235/293] revert changes --- .circleci/config.yml | 1 - lib/build.gradle | 1 - 2 files changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a313263..23f09bb2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,6 @@ workflows: branches: only: - master - - automated-release requires: - build # api-diff: diff --git a/lib/build.gradle b/lib/build.gradle index d7951ff3..3649399c 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -67,7 +67,6 @@ javadoc { dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' - testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' testImplementation 'net.jodah:concurrentunit:0.4.6' From a2ecb759b2b0200f5a61fe9227b5a2ddf804eee7 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Tue, 11 Oct 2022 15:16:41 +0200 Subject: [PATCH 236/293] Update config.yml --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 23f09bb2..e6b1dcd9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@dev:d4bf2df + ship: auth0/ship@0.7.2 codecov: codecov/codecov@3 commands: From 1e9072d5b0f762f8fe83c6674cd9a3a77bc83255 Mon Sep 17 00:00:00 2001 From: noetro Date: Sat, 15 Oct 2022 03:34:26 +0200 Subject: [PATCH 237/293] Optimise TokenUtils parsing (#611) * Optimise parsing of token for well-defined JWT format * Update error message in test to match new code * Fixing checkstyle issues * Added missing test case for no parts * Return new JWTDecodeException Return a new JWTDecodeException from private utility method `wrongNumberOfParts`, instead of throwing, since we throw from `splitToken()`. Co-authored-by: Jim Anderson Co-authored-by: Jim Anderson --- .../main/java/com/auth0/jwt/TokenUtils.java | 32 ++++++++++++---- .../java/com/auth0/jwt/JWTDecoderTest.java | 2 +- .../java/com/auth0/jwt/TokenUtilsTest.java | 38 +++++++++++++++++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/TokenUtils.java b/lib/src/main/java/com/auth0/jwt/TokenUtils.java index 6fa731c5..e62b9832 100644 --- a/lib/src/main/java/com/auth0/jwt/TokenUtils.java +++ b/lib/src/main/java/com/auth0/jwt/TokenUtils.java @@ -15,15 +15,33 @@ static String[] splitToken(String token) throws JWTDecodeException { if (token == null) { throw new JWTDecodeException("The token is null."); } - String[] parts = token.split("\\."); - if (parts.length == 2 && token.endsWith(".")) { - //Tokens with alg='none' have empty String as Signature. - parts = new String[]{parts[0], parts[1], ""}; + + char delimiter = '.'; + + int firstPeriodIndex = token.indexOf(delimiter); + if (firstPeriodIndex == -1) { + throw wrongNumberOfParts(0); } - if (parts.length != 3) { - throw new JWTDecodeException( - String.format("The token was expected to have 3 parts, but got %s.", parts.length)); + + int secondPeriodIndex = token.indexOf(delimiter, firstPeriodIndex + 1); + if (secondPeriodIndex == -1) { + throw wrongNumberOfParts(2); + } + + // too many ? + if (token.indexOf(delimiter, secondPeriodIndex + 1) != -1) { + throw wrongNumberOfParts("> 3"); } + + String[] parts = new String[3]; + parts[0] = token.substring(0, firstPeriodIndex); + parts[1] = token.substring(firstPeriodIndex + 1, secondPeriodIndex); + parts[2] = token.substring(secondPeriodIndex + 1); + return parts; } + + private static JWTDecodeException wrongNumberOfParts(Object partCount) { + return new JWTDecodeException(String.format("The token was expected to have 3 parts, but got %s.", partCount)); + } } diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index 82dc895c..cc427d60 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -50,7 +50,7 @@ public void shouldThrowIfLessThan3Parts() { @Test public void shouldThrowIfMoreThan3Parts() { exception.expect(JWTDecodeException.class); - exception.expectMessage("The token was expected to have 3 parts, but got 4."); + exception.expectMessage("The token was expected to have 3 parts, but got > 3."); JWT.decode("this.has.four.parts"); } diff --git a/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java index 8649ec90..01806ddc 100644 --- a/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java +++ b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java @@ -13,6 +13,30 @@ public class TokenUtilsTest { @Rule public ExpectedException exception = ExpectedException.none(); + @Test + public void toleratesEmptyFirstPart() { + String token = ".eyJpc3MiOiJhdXRoMCJ9.W1mx_Y0hbAMbPmfW9whT605AAcxB7REFuJiDAHk2Sdc"; + String[] parts = TokenUtils.splitToken(token); + + assertThat(parts, is(notNullValue())); + assertThat(parts, is(arrayWithSize(3))); + assertThat(parts[0], is("")); + assertThat(parts[1], is("eyJpc3MiOiJhdXRoMCJ9")); + assertThat(parts[2], is("W1mx_Y0hbAMbPmfW9whT605AAcxB7REFuJiDAHk2Sdc")); + } + + @Test + public void toleratesEmptySecondPart() { + String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0..W1mx_Y0hbAMbPmfW9whT605AAcxB7REFuJiDAHk2Sdc"; + String[] parts = TokenUtils.splitToken(token); + + assertThat(parts, is(notNullValue())); + assertThat(parts, is(arrayWithSize(3))); + assertThat(parts[0], is("eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0")); + assertThat(parts[1], is("")); + assertThat(parts[2], is("W1mx_Y0hbAMbPmfW9whT605AAcxB7REFuJiDAHk2Sdc")); + } + @Test public void shouldSplitToken() { String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9.W1mx_Y0hbAMbPmfW9whT605AAcxB7REFuJiDAHk2Sdc"; @@ -34,19 +58,27 @@ public void shouldSplitTokenWithEmptySignature() { assertThat(parts, is(arrayWithSize(3))); assertThat(parts[0], is("eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0")); assertThat(parts[1], is("eyJpc3MiOiJhdXRoMCJ9")); - assertThat(parts[2], is(isEmptyString())); + assertThat(parts[2], is(emptyString())); } @Test public void shouldThrowOnSplitTokenWithMoreThan3Parts() { exception.expect(JWTDecodeException.class); - exception.expectMessage("The token was expected to have 3 parts, but got 4."); + exception.expectMessage("The token was expected to have 3 parts, but got > 3."); String token = "this.has.four.parts"; TokenUtils.splitToken(token); } @Test - public void shouldThrowOnSplitTokenWithLessThan3Parts() { + public void shouldThrowOnSplitTokenWithNoParts() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The token was expected to have 3 parts, but got 0."); + String token = "notajwt"; + TokenUtils.splitToken(token); + } + + @Test + public void shouldThrowOnSplitTokenWith2Parts() { exception.expect(JWTDecodeException.class); exception.expectMessage("The token was expected to have 3 parts, but got 2."); String token = "two.parts"; From 9840e74b7ef87ce9678d2ee71308499fb1b6f757 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 14 Oct 2022 20:37:24 -0500 Subject: [PATCH 238/293] Update Claim#asString documentation (#615) --- lib/src/main/java/com/auth0/jwt/interfaces/Claim.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index 7c4da4f7..ca5244d6 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -62,9 +62,10 @@ public interface Claim { /** * Get this Claim as a String. - * If the value isn't of type String or it can't be converted to a String, {@code null} will be returned. + * If the value isn't of type String, {@code null} will be returned. For a String representation of non-textual + * claim types, clients can call {@code toString()}. * - * @return the value as a String or null. + * @return the value as a String or null if the underlying value is not a string. */ String asString(); From 1875ed74b136422952080b5f2ac7656e67e51f27 Mon Sep 17 00:00:00 2001 From: "sre-57-opslevel[bot]" <113727212+sre-57-opslevel[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:07:35 +0000 Subject: [PATCH 239/293] Upload OpsLevel YAML --- opslevel.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 opslevel.yml diff --git a/opslevel.yml b/opslevel.yml new file mode 100644 index 00000000..009a5ec0 --- /dev/null +++ b/opslevel.yml @@ -0,0 +1,6 @@ +--- +version: 1 +repository: + owner: dx_sdks + tier: + tags: From e8808abb910bb2d3ea8da816e626a7df9fc543e9 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 19 Oct 2022 10:25:34 -0500 Subject: [PATCH 240/293] Update .shiprc to only update lib version in build.gradle (#625) --- .shiprc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.shiprc b/.shiprc index a00fb91e..2bd0fdb3 100644 --- a/.shiprc +++ b/.shiprc @@ -1,7 +1,7 @@ { "files": { "README.md": [], - "lib/build.gradle": [] + "lib/build.gradle": ["version[[:blank:]]*=[[:blank:]]*{MAJOR}.{MINOR}.{PATCH}"] }, "prefixVersion": false } \ No newline at end of file From 49c5def67ab2d37aa98f210bec2ca5ea39889d8c Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 19 Oct 2022 10:46:57 -0500 Subject: [PATCH 241/293] Re-enable japicmp API diff checking (#619) --- .circleci/config.yml | 42 +++++++++++++++++++++--------------------- lib/build.gradle | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e6b1dcd9..32e9d4de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,14 +23,14 @@ commands: steps: - run: ./gradlew check jacocoTestReport --continue --console=plain - codecov/upload - # run-api-diff: - # steps: - # # run apiDiff task - # - run: ./gradlew apiDiff - # - store_artifacts: - # path: lib/build/reports/apiDiff/apiDiff.txt - # - store_artifacts: - # path: lib/build/reports/apiDiff/apiDiff.html + run-api-diff: + steps: + # run apiDiff task + - run: ./gradlew apiDiff + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.txt + - store_artifacts: + path: lib/build/reports/apiDiff/apiDiff.html jobs: build: docker: @@ -42,16 +42,16 @@ jobs: GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' _JAVA_OPTIONS: "-Xms512m -Xmx1024m" TERM: dumb - # api-diff: - # docker: - # - image: openjdk:11.0-jdk - # steps: - # - checkout-and-build - # - run-api-diff - # environment: - # GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - # _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - # TERM: dumb + api-diff: + docker: + - image: openjdk:11.0-jdk + steps: + - checkout-and-build + - run-api-diff + environment: + GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' + _JAVA_OPTIONS: "-Xms512m -Xmx1024m" + TERM: dumb workflows: build-and-test: @@ -68,6 +68,6 @@ workflows: - master requires: - build - # api-diff: - # jobs: - # - api-diff + api-diff: + jobs: + - api-diff diff --git a/lib/build.gradle b/lib/build.gradle index 3649399c..a0d7a7da 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -28,7 +28,7 @@ oss { repository "java-jwt" organization "auth0" description "Java implementation of JSON Web Token (JWT)" - baselineCompareVersion "4.0.0" + baselineCompareVersion "4.1.0" skipAssertSigningConfiguration true developers { From 3526fcc80ec15038de1a13a2c84d55d2453c835a Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 19 Oct 2022 21:53:07 -0500 Subject: [PATCH 242/293] Release 4.2.0 (#626) --- CHANGELOG.md | 12 ++++++++++++ README.md | 4 ++-- lib/build.gradle | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04796fd7..c6c2d511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Change Log +## [4.2.0](https://github.com/auth0/java-jwt/tree/4.2.0) (2022-10-19) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.1.0...4.2.0) + +**Changed** +- Re-enable japicmp API diff checking [\#619](https://github.com/auth0/java-jwt/pull/619) ([jimmyjames](https://github.com/jimmyjames)) +- Update .shiprc to only update lib version in build.gradle [\#625](https://github.com/auth0/java-jwt/pull/625) ([jimmyjames](https://github.com/jimmyjames)) +- Optimise TokenUtils parsing [\#611](https://github.com/auth0/java-jwt/pull/611) ([noetro](https://github.com/noetro)) +- Update Circle Ship Orb configuration [\#616](https://github.com/auth0/java-jwt/pull/616) ([frederikprijck](https://github.com/frederikprijck)) + +**Fixed** +- Update Claim#asString documentation [\#615](https://github.com/auth0/java-jwt/pull/615) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.1.0](https://github.com/auth0/java-jwt/tree/4.1.0) (2022-10-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.0.0...4.1.0) diff --git a/README.md b/README.md index a4fa509e..89b67ad8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This library is supported for Java 8, 11, and 17. For issues on non-LTS versions ### Gradle ```gradle -implementation 'com.auth0:java-jwt:4.1.0' +implementation 'com.auth0:java-jwt:4.2.0' ``` ### Maven @@ -48,7 +48,7 @@ implementation 'com.auth0:java-jwt:4.1.0' com.auth0 java-jwt - 4.1.0 + 4.2.0 ``` diff --git a/lib/build.gradle b/lib/build.gradle index a0d7a7da..05807472 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,5 @@ buildscript { - version = "4.1.0" + version = "4.2.0" } plugins { From 32935767d3414d1e91feec41ee69159b3d2524eb Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 20 Oct 2022 17:08:35 -0500 Subject: [PATCH 243/293] Fix .shiprc build.gradle version substitution pattern (#627) --- .shiprc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.shiprc b/.shiprc index 2bd0fdb3..fe59345e 100644 --- a/.shiprc +++ b/.shiprc @@ -1,7 +1,7 @@ { "files": { "README.md": [], - "lib/build.gradle": ["version[[:blank:]]*=[[:blank:]]*{MAJOR}.{MINOR}.{PATCH}"] + "lib/build.gradle": ["version = \"{MAJOR}.{MINOR}.{PATCH}\""] }, "prefixVersion": false } \ No newline at end of file From 3d3980de9c0a84b6265de9263647cdbfda7c8a5b Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Sat, 22 Oct 2022 21:02:48 -0500 Subject: [PATCH 244/293] Update ship orb (#629) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 32e9d4de..9cf198c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@0.7.2 + ship: auth0/ship@0.7.3 codecov: codecov/codecov@3 commands: From c8d0ba995543c9cee50abe9f4aa4f9873b29489f Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 24 Oct 2022 09:14:06 -0500 Subject: [PATCH 245/293] Bump `com.fasterxml.jackson.core:jackson-databind` to 2.13.4.2 (#630) Resolves https://www.cve.org/CVERecord?id=CVE-2022-42003 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 05807472..9cc4349c 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -65,7 +65,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' From 3cff66f1a23b51ab85ae4d7c5c1837b90677c75e Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 24 Oct 2022 16:52:15 -0500 Subject: [PATCH 246/293] Use latest ship orb (#634) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9cf198c8..6c87574b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2.1 orbs: - ship: auth0/ship@0.7.3 + ship: auth0/ship@0 codecov: codecov/codecov@3 commands: From 4c2533dbd0bc1aa714ac7d0e291652cdaecdd265 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Tue, 25 Oct 2022 06:08:09 +0400 Subject: [PATCH 247/293] Remove emoji to fix the broken documentation link (#632) Co-authored-by: Jim Anderson --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89b67ad8..f96e806f 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); > Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). -##### :key: HMAC Key Length and Security +##### HMAC Key Length and Security When using a Hash-based Message Authentication Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recommendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. From 1e38286a52936d7ccdae3f457369389ad7474b7a Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Tue, 25 Oct 2022 07:26:20 -0500 Subject: [PATCH 248/293] Release 4.2.1 (#636) --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- lib/build.gradle | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c2d511..ed3706dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log +## [4.2.1](https://github.com/auth0/java-jwt/tree/4.2.1) (2022-10-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.0...4.2.1) + +**Security** +- Use latest ship orb [\#634](https://github.com/auth0/java-jwt/pull/634) ([jimmyjames](https://github.com/jimmyjames)) +- Bump `com.fasterxml.jackson.core:jackson-databind` to 2.13.4.2 [\#630](https://github.com/auth0/java-jwt/pull/630) ([evansims](https://github.com/evansims)) + ## [4.2.0](https://github.com/auth0/java-jwt/tree/4.2.0) (2022-10-19) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.1.0...4.2.0) diff --git a/README.md b/README.md index f96e806f..bfa4345d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ This library is supported for Java 8, 11, and 17. For issues on non-LTS versions ### Gradle ```gradle -implementation 'com.auth0:java-jwt:4.2.0' +implementation 'com.auth0:java-jwt:4.2.1' ``` ### Maven @@ -48,7 +48,7 @@ implementation 'com.auth0:java-jwt:4.2.0' com.auth0 java-jwt - 4.2.0 + 4.2.1 ``` diff --git a/lib/build.gradle b/lib/build.gradle index 9cc4349c..d6c9b061 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,5 +1,5 @@ buildscript { - version = "4.2.0" + version = "4.2.1" } plugins { From d0ebc3f50581f3341a9b214f32612926b8b1a201 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Wed, 26 Oct 2022 03:38:39 -0500 Subject: [PATCH 249/293] [SDK-3694] README redesign (#617) --- EXAMPLES.md | 130 +++++++++++++ README.md | 523 ++++++++-------------------------------------------- 2 files changed, 206 insertions(+), 447 deletions(-) create mode 100644 EXAMPLES.md diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..8a849303 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,130 @@ +# Examples using java-jwt + +* [Inspecting a DecodedJWT](#inspecting-a-decodedjwt) +* [DateTime Claim Validation](#datetime-claim-validation) +* [Using custom claims](#using-custom-claims) +* [Using a KeyProvider](#using-a-keyprovider) + +## Inspecting a DecodedJWT + +The successful verification of a JWT returns a `DecodedJWT`, from which you can obtain its contents. + +```java +DecodedJWT jwt = JWT.require(algorithm) + .build() + .verify("a.b.c"); + +// standard claims can be retrieved through first-class methods +String subject = jwt.getSubject(); +String aud = jwt.getAudience(); +// ... + +// custom claims can also be obtained +String customStringClaim = jwt.getClaim("custom-string-claim").asString(); +``` + +When retrieving custom claims, a [Claim](https://javadoc.io/doc/com.auth0/java-jwt/latest/com/auth0/jwt/interfaces/Claim.html) is returned, which can then be used to obtain the value depending on the value's underlying type. + +## DateTime Claim Validation + +A JWT token may include DateNumber fields that can be used to validate that: + +* The token was issued in a past date `"iat" < NOW` +* The token hasn't expired yet `"exp" > NOW` +* The token can already be used. `"nbf" < NOW` + +When verifying a JWT, the standard DateTime claims are validated by default. A `JWTVerificationException` is thrown if any of the claim values are invalid. + +To specify a **leeway** in which the JWT should still be considered valid, use the `acceptLeeway()` method in the `JWTVerifier` builder and pass a positive seconds value. This applies to every item listed above. + +```java +JWTVerifier verifier = JWT.require(algorithm) + .acceptLeeway(1) // 1 sec for nbf, iat and exp + .build(); +``` + +You can also specify a custom value for a given DateTime claim and override the default one for only that claim. + +```java +JWTVerifier verifier = JWT.require(algorithm) + .acceptLeeway(1) //1 sec for nbf and iat + .acceptExpiresAt(5) //5 secs for exp + .build(); +``` + +If you need to test this behavior in your application, cast the `Verification` instance to a `BaseVerification` to gain visibility of the `verification.build()` method that accepts a `java.time.Clock`. e.g.: + +```java +BaseVerification verification = (BaseVerification) JWT.require(algorithm) + .acceptLeeway(1) + .acceptExpiresAt(5); +private final Clock mockNow = Clock.fixed(Instant.ofEpochSecond(1477592), ZoneId.of("UTC")); +JWTVerifier verifier = verification.build(clock); +``` + +## Using custom claims + +### JWT creation +A JWT can be built with custom payload and header claims, by using the `withHeader` and `withClaim` methods. + +```java +String jwt = JWT.create() + .withHeader(headerMap) + .withClaim("string-claim", "string-value") + .withClaim("number-claim", 42) + .withClaim("bool-claim", true) + .withClaim("datetime-claim", Instant.now()) + .sign(algorithm); +``` + +See the [JavaDoc](https://javadoc.io/doc/com.auth0/java-jwt/latest/com/auth0/jwt/JWTCreator.Builder.html) for all available custom claim methods. + +### JWT verification + +You can also verify a JWT's custom claims: + +```java +JWTVerifier verifier = JWT.require(algorithm) + .withClaim("number-claim", 123) + .withClaimPresence("some-claim-that-just-needs-to-be-present") + .withClaim("predicate-claim", (claim, decodedJWT) -> "custom value".equals(claim.asString())) + .build(); +DecodedJWT jwt = verifier.verify("my.jwt.token"); +``` + +See the [JavaDoc](https://javadoc.io/doc/com.auth0/java-jwt/latest/com/auth0/jwt/JWTVerifier.BaseVerification.html) for all available custom claim verification methods. + +## Using a KeyProvider + +A `KeyProvider` can be used to obtain the keys needed for signing and verifying a JWT. How these keys are constructed are beyond the scope of this library, but the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library provides the ability to obtain the public key from a JWK. +The example below demonstrates this for the RSA algorithm (`ECDSAKeyProvider` can be used for ECDSA). + +```java +JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build(); +final RSAPrivateKey privateKey = // private key +final String privateKeyId = // private key ID + +RSAKeyProvider keyProvider = new RSAKeyProvider() { + @Override + public RSAPublicKey getPublicKeyById(String kid) { + return (RSAPublicKey) jwkProvider.get(kid).getPublicKey(); + } + + @Override + public RSAPrivateKey getPrivateKey() { + // return the private key used + return rsaPrivateKey; + } + + @Override + public String getPrivateKeyId() { + return rsaPrivateKeyId; + } +}; + +Algorithm algorithm = Algorithm.RSA256(keyProvider); +//Use the Algorithm to create and verify JWTs. +``` \ No newline at end of file diff --git a/README.md b/README.md index bfa4345d..7786cbaf 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,26 @@ - - -# Java JWT +![A Java implementation of JSON Web Token (JWT) - RFC 7519.](https://cdn.auth0.com/website/sdks/banners/java-jwt-banner.png) [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) -[![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org) -[![Javadoc](https://javadoc.io/badge2/com.auth0/java-jwt/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) - -A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). +[![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org/) +[![Maven Central](https://img.shields.io/maven-central/v/com.auth0/java-jwt.svg?style=flat-square)](https://mvnrepository.com/artifact/com.auth0/java-jwt) +[![javadoc](https://javadoc.io/badge2/com.auth0/auth0/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt) -> :warning: **Important security note:** JVM has a critical vulnerability for ECDSA Algorithms - [CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449). Please review the details of the vulnerability and update your environment. - -If you're looking for an **Android** version of the JWT Decoder take a look at our [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android) library. +:books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :computer: [API Reference](#api-reference) :speech_balloon: [Feedback](#feedback) -> You are viewing the documentation for the v4 beta release. For the latest stable release, please see the [version 3.x documentation](https://github.com/auth0/java-jwt). +## Documentation +- [Examples](./EXAMPLES.md) - code samples for common java-jwt scenarios. +- [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0. -## Table of Contents -- [**Requirements**](#requirements) -- [**Installation**](#installation) -- [**Available Algorithms**](#available-algorithms) -- [**Quickstart**](#quickstart) - + [**Create and Sign a Token**](#create-and-sign-a-token) - + [**Verify a Token**](#verify-a-token) -- [**Usage**](#usage) - + [**Pick the algorithm**](#pick-the-algorithm) - + [**Time Validation**](#time-validation) - + [**Header Claims**](#header-claims) - + [**Payload Claims**](#payload-claims) - + [**Claim Class**](#claim-class) -- [**Javadoc**](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) +## Getting Started -## Requirements +### Requirements -This library is supported for Java 8, 11, and 17. For issues on non-LTS versions above 8, consideration will be given on a case-by-case basis. +This library is supported for Java LTS versions 8, 11, and 17. For issues on non-LTS versions above 8, consideration will be given on a case-by-case basis. -## Installation +> `java-jwt` is intended for server-side JVM applications. Android applications should use [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android). -### Gradle - -```gradle -implementation 'com.auth0:java-jwt:4.2.1' -``` - -### Maven - -```xml - - com.auth0 - java-jwt - 4.2.1 - -``` - -## Available Algorithms - -The library implements JWT Verification and Signing using the following algorithms: +`java-jwt` supports the following algorithms for both signing and verification: | JWS | Algorithm | Description | | :-------------: | :-------------: | :----- | @@ -70,434 +36,97 @@ The library implements JWT Verification and Signing using the following algorith > Note - Support for ECDSA with curve secp256k1 and SHA-256 (ES256K) has been dropped since it has been [disabled in Java 15](https://www.oracle.com/java/technologies/javase/15-relnote-issues.html#JDK-8237219) -## Quickstart - -### Create and Sign a Token - -You'll first need to create a `JWTCreator` instance by calling `JWT.create()`. Use the builder to define the custom Claims your token needs to have. Finally to get the String token call `sign()` and pass the `Algorithm` instance. - -* Example using `HS256` - - ```java - try { - Algorithm algorithm = Algorithm.HMAC256("secret"); - String token = JWT.create() - .withIssuer("auth0") - .sign(algorithm); - } catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. - } - ``` - -* Example using `RS256` - - ```java - RSAPublicKey publicKey = //Get the key instance - RSAPrivateKey privateKey = //Get the key instance - try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); - String token = JWT.create() - .withIssuer("auth0") - .sign(algorithm); - } catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. - } - ``` - -If a Claim couldn't be converted to JSON or the Key used in the signing process was invalid a `JWTCreationException` will raise. - -### Verify a Token - -You'll first need to create a `JWTVerifier` instance by calling `JWT.require()` and passing the `Algorithm` instance. If you require the token to have specific Claim values, use the builder to define them. The instance returned by the method `build()` is reusable, so you can define it once and use it to verify different tokens. Finally call `verifier.verify()` passing the token. - -* Example using `HS256` - - ```java - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; - try { - Algorithm algorithm = Algorithm.HMAC256("secret"); //use more secure key - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); - } catch (JWTVerificationException exception){ - //Invalid signature/claims - } - ``` - -* Example using `RS256` - - ```java - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; - RSAPublicKey publicKey = //Get the key instance - RSAPrivateKey privateKey = //Get the key instance - try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); - } catch (JWTVerificationException exception){ - //Invalid signature/claims - } - ``` - -If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise. - -
-Need to peek into a JWT without verifying it? (Click to expand) - -### Decode a Token - -> __Warning:__ This will __not__ verify whether the signature is valid. You should __not__ use this for untrusted messages. You most likely want to use `JWTVerifier` as documented above instead. - -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -try { - DecodedJWT jwt = JWT.decode(token); -} catch (JWTDecodeException exception){ - //Invalid token -} -``` - -If the token has an invalid syntax or the header or payload are not JSONs, a `JWTDecodeException` will raise. - -
- -## Usage - -### Pick the Algorithm - -The Algorithm defines how a token is signed and verified. It can be instantiated with the raw value of the secret in the case of HMAC algorithms, or the key pairs or `KeyProvider` in the case of RSA and ECDSA algorithms. Once created, the instance is reusable for token signing and verification operations. - -When using RSA or ECDSA algorithms and you just need to **sign** JWTs you can avoid specifying a Public Key by passing a `null` value. The same can be done with the Private Key when you just need to **verify** JWTs. - - -#### Using static secrets or keys: - -```java -//HMAC -Algorithm algorithmHS = Algorithm.HMAC256("secret"); //use more secure key - -//RSA -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance -Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey); -``` - -> Note: How you obtain or read keys is not in the scope of this library. For an example of how you might implement this, see [this gist](https://gist.github.com/lbalmaceda/9a0c7890c2965826c04119dcfb1a5469). - -##### HMAC Key Length and Security - -When using a Hash-based Message Authentication Code, e.g. HS256 or HS512, in order to comply with the strict requirements of the JSON Web Algorithms (JWA) specification (RFC7518), you **must** use a secret key which has the same (or larger) bit length as the size of the output hash. This is to avoid weakening the security strength of the authentication code (see NIST recommendations NIST SP 800-117). For example, when using HMAC256, the secret key length must be a minimum of 256 bits. - -#### Using a KeyProvider: -By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: - -- `getPublicKeyById(String kid)`: Its called during token signature verification and it should return the key used to verify the token. If key rotation is being used, e.g. [JWK](https://tools.ietf.org/html/rfc7517) it can fetch the correct rotation key using the id. (Or just return the same key all the time). -- `getPrivateKey()`: Its called during token signing and it should return the key that will be used to sign the JWT. -- `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. - - -The following example shows how this would work with `JwkProvider` from the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. - -```java -final JwkProvider jwkProvider = new UrlJwkProvider("https://samples.auth0.com/"); -final RSAPrivateKey privateKey = //Get the key instance -final String privateKeyId = //Create an Id for the above key - -RSAKeyProvider keyProvider = new RSAKeyProvider() { - @Override - public RSAPublicKey getPublicKeyById(String kid) { - //Received 'kid' value might be null if it wasn't defined in the Token's header - PublicKey publicKey = jwkProvider.get(kid).getPublicKey(); - return (RSAPublicKey) publicKey; - } - - @Override - public RSAPrivateKey getPrivateKey() { - return privateKey; - } - - @Override - public String getPrivateKeyId() { - return privateKeyId; - } -}; - -Algorithm algorithm = Algorithm.RSA256(keyProvider); -//Use the Algorithm to create and verify JWTs. -``` - - -### Time Validation - -The JWT token may include DateNumber fields that can be used to validate that: -* The token was issued in a past date `"iat" < TODAY` -* The token hasn't expired yet `"exp" > TODAY` and -* The token can already be used. `"nbf" < TODAY` - -When verifying a token the time validation occurs automatically, resulting in a `JWTVerificationException` being throw when the values are invalid. If any of the previous fields are missing they won't be considered in this validation. - -To specify a **leeway window** in which the Token should still be considered valid, use the `acceptLeeway()` method in the `JWTVerifier` builder and pass a positive seconds value. This applies to every item listed above. - -```java -JWTVerifier verifier = JWT.require(algorithm) - .acceptLeeway(1) // 1 sec for nbf, iat and exp - .build(); -``` - -You can also specify a custom value for a given Date claim and override the default one for only that claim. - -```java -JWTVerifier verifier = JWT.require(algorithm) - .acceptLeeway(1) //1 sec for nbf and iat - .acceptExpiresAt(5) //5 secs for exp - .build(); -``` - -If you need to test this behavior in your lib/app cast the `Verification` instance to a `BaseVerification` to gain visibility of the `verification.build()` method that accepts a `java.time.Clock`. e.g.: - -```java -BaseVerification verification = (BaseVerification) JWT.require(algorithm) - .acceptLeeway(1) - .acceptExpiresAt(5); -private final Clock mockNow = Clock.fixed(Instant.ofEpochSecond(1477592), ZoneId.of("UTC")); -JWTVerifier verifier = verification.build(clock); -``` - -### Header Claims - -#### Algorithm ("alg") - -Returns the Algorithm value or null if it's not defined in the Header. - -```java -String algorithm = jwt.getAlgorithm(); -``` - -#### Type ("typ") - -Returns the Type value or null if it's not defined in the Header. - -```java -String type = jwt.getType(); -``` - -#### Content Type ("cty") - -Returns the Content Type value or null if it's not defined in the Header. - -```java -String contentType = jwt.getContentType(); -``` - -#### Key Id ("kid") - -Returns the Key Id value or null if it's not defined in the Header. - -```java -String keyId = jwt.getKeyId(); -``` - -#### Private Claims - -Additional Claims defined in the token's Header can be obtained by calling `getHeaderClaim()` and passing the Claim name. A Claim will always be returned, even if it can't be found. You can check if a Claim's value is null by calling `claim.isNull()`. - -```java -Claim claim = jwt.getHeaderClaim("owner"); -``` - -When creating a Token with the `JWT.create()` you can specify header Claims by calling `withHeader()` and passing both the map of claims. - -```java -Map headerClaims = new HashMap(); -headerClaims.put("owner", "auth0"); -String token = JWT.create() - .withHeader(headerClaims) - .sign(algorithm); -``` - -> The `alg` and `typ` values will always be included in the Header after the signing process. - - -### Payload Claims - -#### Issuer ("iss") - -Returns the Issuer value or null if it's not defined in the Payload. - -```java -String issuer = jwt.getIssuer(); -``` - -#### Subject ("sub") - -Returns the Subject value or null if it's not defined in the Payload. - -```java -String subject = jwt.getSubject(); -``` - -#### Audience ("aud") - -Returns the Audience value or null if it's not defined in the Payload. - -```java -List audience = jwt.getAudience(); -``` - -#### Expiration Time ("exp") - -Returns the Expiration Time value or null if it's not defined in the Payload. - -```java -Date expiresAt = jwt.getExpiresAt(); -``` - -If you prefer to work with `java.time.Instant` instead of `java.util.Date`: - -```java -Instant expiresAt = jwt.getExpiresAtAsInstant(); -``` - -#### Not Before ("nbf") - -Returns the Not Before value or null if it's not defined in the Payload. - -```java -Date notBefore = jwt.getNotBefore(); -``` - -If you prefer to work with `java.time.Instant` instead of `java.util.Date`: - -```java -Instant notBefore = jwt.getNotBeforeAsInstant(); -``` - -#### Issued At ("iat") - -Returns the Issued At value or null if it's not defined in the Payload. - -```java -Date issuedAt = jwt.getIssuedAt(); -``` - -If you prefer to work with `java.time.Instant` instead of `java.util.Date`: - -```java -Instant issuedAt = jwt.getIssuedAtAsInstant(); -``` - -#### JWT ID ("jti") +> :warning: **Important security note:** JVM has a critical vulnerability for ECDSA Algorithms - [CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449). Please review the details of the vulnerability and update your environment. +### Installation -Returns the JWT ID value or null if it's not defined in the Payload. +Add the dependency via Maven: -```java -String id = jwt.getId(); +```xml + + com.auth0 + java-jwt + 4.2.1 + ``` -#### Private Claims +or Gradle: -Additional Claims defined in the token's Payload can be obtained by calling `getClaims()` or `getClaim()` and passing the Claim name. A Claim will always be returned, even if it can't be found. You can check if a Claim's value is null by calling `claim.isNull()`. - -```java -Map claims = jwt.getClaims(); //Key is the Claim name -Claim claim = claims.get("isAdmin"); +```gradle +implementation 'com.auth0:jva-jwt:4.2.1' ``` -or +### Create a JWT -```java -Claim claim = jwt.getClaim("isAdmin"); -``` +Use `JWT.create()`, configure the claims, and then call `sign(algorithm)` to sign the JWT. -When creating a Token with the `JWT.create()` you can specify a custom Claim by calling `withClaim()` and passing both the name and the value. +The example below demonstrates this using the `RS256` signing algorithm: ```java -String token = JWT.create() - .withClaim("name", 123) - .withArrayClaim("array", new Integer[]{1, 2, 3}) - .withNullClaim("claim_name") +try { + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); + String token = JWT.create() + .withIssuer("auth0") .sign(algorithm); +} catch (JWTCreationException exception){ + // Invalid Signing configuration / Couldn't convert Claims. +} ``` -You can also create a JWT by calling `withPayload()` and passing a map of claim names to values: +### Verify a JWT -```java -Map payloadClaims = new HashMap<>(); -payloadClaims.put("@context", "https://auth0.com/"); -String token = JWT.create() - .withPayload(payloadClaims) - .sign(algorithm); -``` +Create a `JWTVerifier` passing the `Algorithm`, and specify any required claim values. -You can also verify custom Claims on the `JWT.require()` by calling `withClaim()` and passing both the name and the required value. +The following example uses `RS256` to verify the JWT. ```java -JWTVerifier verifier = JWT.require(algorithm) - .withClaim("name", 123) - .withArrayClaim("array", 1, 2, 3) - .withNullClaim("null_value") //checks if the claim name provided has null value - .withClaimPresence("claim_presence") //checks if the claim name provided is in the payload - .withClaim("predicate", (claim, decodedJWT) -> "custom_check".equals(claim.asString())) //can be used to run custom verification - .build(); -DecodedJWT jwt = verifier.verify("my.jwt.token"); +String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; +DecodedJWT decodedJWT; +try { + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); + JWTVerifier verifier = JWT.require(algorithm) + // specify an specific claim validations + .withIssuer("auth0") + // reusable verifier instance + .build(); + + decodedJWT jwt = verifier.verify(token); +} catch (JWTVerificationException exception){ + // Invalid signature/claims +} ``` -> Currently supported classes for custom JWT Claim creation and verification are: Boolean, Integer, Double, String, Date and Arrays of type String and Integer. - - -### Claim Class -The Claim class is a wrapper for the Claim values. It allows you to get the Claim as different class types. The available helpers are: - -#### Primitives -* **asBoolean()**: Returns the Boolean value or null if it can't be converted. -* **asInt()**: Returns the Integer value or null if it can't be converted. -* **asDouble()**: Returns the Double value or null if it can't be converted. -* **asLong()**: Returns the Long value or null if it can't be converted. -* **asString()**: Returns the String value or null if it can't be converted. -* **asInstant()**: Returns the Instant value or null if it can't be converted. -* **asDate()**: Returns the Date value or null if it can't be converted. - -> For `asInstant()` and `asDate()` the value must be a NumericDate (Unix Epoch/Timestamp). Note that the [JWT Standard](https://tools.ietf.org/html/rfc7519#section-2) specified that all the *NumericDate* values must be in seconds. - -#### Custom Classes and Collections -To obtain a Claim as a Collection you'll need to provide the **Class Type** of the contents to convert from. - -* **as(class)**: Returns the value parsed as **Class Type**. For collections you should use the `asArray` and `asList` methods. -* **asMap()**: Returns the value parsed as **Map**. -* **asArray(class)**: Returns the value parsed as an Array of elements of type **Class Type**, or null if the value isn't a JSON Array. -* **asList(class)**: Returns the value parsed as a List of elements of type **Class Type**, or null if the value isn't a JSON Array. - -If the values can't be converted to the given **Class Type** a `JWTDecodeException` will raise. - - +If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will be thrown. -## What is Auth0? +See the [examples](./EXAMPLES.md) and [JavaDocs](https://javadoc.io/doc/com.auth0/java-jwt/latest) for additional documentation. -Auth0 helps you to: +## API Reference -* Add authentication with [multiple authentication sources](https://auth0.com/docs/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, among others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. -* Add authentication through more traditional **[username/password databases](https://auth0.com/docs/connections/database)**. -* Add support for **[linking different user accounts](https://auth0.com/docs/users/user-account-linking)** with the same user. -* Support for generating signed [Json Web Tokens](https://auth0.com/docs/tokens/json-web-tokens) to call your APIs and **flow the user identity** securely. -* Analytics of how, when and where users are logging in. -* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://auth0.com/docs/rules). +- [java-jwt JavaDocs](https://javadoc.io/doc/com.auth0/java-jwt/latest) -## Create a free account in Auth0 +## Feedback -1. Go to [Auth0](https://auth0.com) and click Sign Up. -2. Use Google, GitHub or Microsoft Account to login. +### Contributing -## Issue Reporting +We appreciate feedback and contribution to this repo! Before you get started, please see the following: -If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. +- [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +- [Auth0's code of conduct guidelines]((https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)) -## Author +### Raise an issue +To provide feedback or report a bug, [please raise an issue on our issue tracker](https://github.com/auth0/java-jwt/issues). -[Auth0](https://auth0.com/) +### Vulnerability Reporting +Please do not report security vulnerabilities on the public Github issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. -## License +--- -This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. +

+ + + + Auth0 Logo + +

+

Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0?

+

+This project is licensed under the MIT license. See the LICENSE file for more info.

From 56e663c3b5830e0329160f2165043aff89f8d3bb Mon Sep 17 00:00:00 2001 From: Jeff Allen <103958546+trestletech-dd@users.noreply.github.com> Date: Wed, 26 Oct 2022 16:03:25 -0500 Subject: [PATCH 250/293] Fix the spelling of the dependency name in README (#638) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7786cbaf..8e634ee6 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Add the dependency via Maven: or Gradle: ```gradle -implementation 'com.auth0:jva-jwt:4.2.1' +implementation 'com.auth0:java-jwt:4.2.1' ``` ### Create a JWT From 931cd1243c4653744c0f1cc59f182566139cd2c5 Mon Sep 17 00:00:00 2001 From: Wilson Chuks Date: Wed, 23 Nov 2022 07:17:22 +0900 Subject: [PATCH 251/293] Fixed a typographical error on the readme file (#641) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e634ee6..903e37ba 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ try { // reusable verifier instance .build(); - decodedJWT jwt = verifier.verify(token); + decodedJWT = verifier.verify(token); } catch (JWTVerificationException exception){ // Invalid signature/claims } From 14618b5af545e382ce734ef3cb54ba12398460dc Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 11 Jan 2023 14:32:18 +0530 Subject: [PATCH 252/293] Temporarily disable auto release --- .circleci/config.yml | 12 ------------ lib/build.gradle | 11 ----------- 2 files changed, 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c87574b..1675c39f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,5 @@ version: 2.1 orbs: - ship: auth0/ship@0 codecov: codecov/codecov@3 commands: @@ -57,17 +56,6 @@ workflows: build-and-test: jobs: - build - - ship/java-publish: - prefix-tag: false - context: - - publish-gh - - publish-sonatype - filters: - branches: - only: - - master - requires: - - build api-diff: jobs: - api-diff diff --git a/lib/build.gradle b/lib/build.gradle index d6c9b061..6b2fdfe3 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,6 +1,3 @@ -buildscript { - version = "4.2.1" -} plugins { id 'java' @@ -9,13 +6,6 @@ plugins { id 'checkstyle' } -def signingKey = findProperty('signingKey') -def signingKeyPwd = findProperty('signingPassword') - -signing { - useInMemoryPgpKeys(signingKey, signingKeyPwd) -} - checkstyle { toolVersion '10.0' checkstyleTest.enabled = false //We are disabling lint checks for tests @@ -29,7 +19,6 @@ oss { organization "auth0" description "Java implementation of JSON Web Token (JWT)" baselineCompareVersion "4.1.0" - skipAssertSigningConfiguration true developers { auth0 { From b4fae643ad27ed5efb28a12da7ac7d52f9917706 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Wed, 11 Jan 2023 17:50:24 +0100 Subject: [PATCH 253/293] Release 4.2.2 (#650) --- CHANGELOG.md | 6 ++++++ README.md | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed3706dc..eaf3feec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [4.2.2](https://github.com/auth0/java-jwt/tree/4.2.2) (2023-01-11) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.1...4.2.2) + +This patch release does not contain any functional changes, but is being released using an updated signing key for verification as part of our commitment to best security practices. +Please review [the README note for additional details.](https://github.com/auth0/java-jwt/blob/master/README.md) + ## [4.2.1](https://github.com/auth0/java-jwt/tree/4.2.1) (2022-10-24) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.0...4.2.1) diff --git a/README.md b/README.md index 903e37ba..8583ed7a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +> **Note** +> As part of our ongoing commitment to best security practices, we have rotated the signing keys used to sign previous releases of this SDK. As a result, new patch builds have been released using the new signing key. Please upgrade at your earliest convenience. +> +> While this change won't affect most developers, if you have implemented a dependency signature validation step in your build process, you may notice a warning that past releases can't be verified. This is expected, and a result of the key rotation process. Updating to the latest version will resolve this for you. + ![A Java implementation of JSON Web Token (JWT) - RFC 7519.](https://cdn.auth0.com/website/sdks/banners/java-jwt-banner.png) [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) @@ -45,14 +50,14 @@ Add the dependency via Maven: com.auth0 java-jwt - 4.2.1 + 4.2.2 ``` or Gradle: ```gradle -implementation 'com.auth0:java-jwt:4.2.1' +implementation 'com.auth0:java-jwt:4.2.2' ``` ### Create a JWT From 6e80ea1e5a8624f34f133c3be71078ee9ab83906 Mon Sep 17 00:00:00 2001 From: CodeDead Date: Thu, 26 Jan 2023 22:44:56 +0100 Subject: [PATCH 254/293] Feature/cleanup (#642) * GIT compliance * Simpler HashMap declaration * Replaced usage of deprecated JsonMapper methods, removed unneeded null check * Removed unused import * Removed unused imports Co-authored-by: Jim Anderson --- EXAMPLES.md | 2 +- LICENSE | 2 +- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 11 +++++++---- .../java/com/auth0/jwt/algorithms/RSAAlgorithm.java | 1 - lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java | 2 +- .../test/java/com/auth0/jwt/ConcurrentVerifyTest.java | 3 --- lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java | 2 -- .../com/auth0/jwt/algorithms/HMACAlgorithmTest.java | 1 - settings.gradle | 2 +- 9 files changed, 11 insertions(+), 15 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 8a849303..995e4c1d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -127,4 +127,4 @@ RSAKeyProvider keyProvider = new RSAKeyProvider() { Algorithm algorithm = Algorithm.RSA256(keyProvider); //Use the Algorithm to create and verify JWTs. -``` \ No newline at end of file +``` diff --git a/LICENSE b/LICENSE index 4a7a13ad..bcd1854c 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index a99f0fa0..7ed83940 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import java.nio.charset.StandardCharsets; @@ -31,12 +32,14 @@ public final class JWTCreator { private static final SimpleModule module; static { - mapper = new ObjectMapper(); module = new SimpleModule(); module.addSerializer(PayloadClaimsHolder.class, new PayloadSerializer()); module.addSerializer(HeaderClaimsHolder.class, new HeaderSerializer()); - mapper.registerModule(module); - mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + + mapper = JsonMapper.builder() + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .build() + .registerModule(module); } private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) @@ -489,7 +492,7 @@ private static boolean validateClaim(Map map) { return false; } - if (entry.getKey() == null || !(entry.getKey() instanceof String)) { + if (!(entry.getKey() instanceof String)) { return false; } } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index 0c7a5b57..ca892e60 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -5,7 +5,6 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.RSAKeyProvider; -import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; diff --git a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java index 3746dcd2..8031d8c0 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java +++ b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java @@ -37,7 +37,7 @@ class BasicHeader implements Header, Serializable { this.type = type; this.contentType = contentType; this.keyId = keyId; - this.tree = Collections.unmodifiableMap(tree == null ? new HashMap() : tree); + this.tree = Collections.unmodifiableMap(tree == null ? new HashMap<>() : tree); this.objectReader = objectReader; } diff --git a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java index a06df7b7..32ede1de 100644 --- a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java +++ b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java @@ -10,14 +10,11 @@ import org.junit.rules.ExpectedException; import java.security.interfaces.ECKey; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; import java.util.Collections; import java.util.List; import java.util.concurrent.*; -import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; //@Ignore("Skipping concurrency tests") diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 020e5e37..8fc83b44 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -16,9 +16,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 4a0269cc..9b6ac0c0 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -13,7 +13,6 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import static com.auth0.jwt.algorithms.CryptoTestHelper.asJWT; import static com.auth0.jwt.algorithms.CryptoTestHelper.assertSignaturePresent; diff --git a/settings.gradle b/settings.gradle index a4cda2d7..8d5f112c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,4 +8,4 @@ pluginManagement { } include ':java-jwt' -project(':java-jwt').projectDir = new File(rootProject.projectDir, '/lib') \ No newline at end of file +project(':java-jwt').projectDir = new File(rootProject.projectDir, '/lib') From 12ae664a60d5e12d824c0896d5a82ef8522a8b69 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 26 Jan 2023 17:04:32 -0600 Subject: [PATCH 255/293] Fix for `exp` claim considered valid if equal to now (#652) exp claim cannot be equal to now --- .../main/java/com/auth0/jwt/JWTVerifier.java | 6 ++-- lib/src/test/java/com/auth0/jwt/JWTTest.java | 5 ++-- .../java/com/auth0/jwt/JWTVerifierTest.java | 30 +++++++++++++++++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 07c86a4c..6cec2026 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -346,7 +346,7 @@ private boolean assertValidInstantClaim(String claimName, Claim claim, long leew throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal), claimVal); } } else { - isValid = assertInstantIsPast(claimVal, leeway, now); + isValid = assertInstantIsLessThanOrEqualToNow(claimVal, leeway, now); if (!isValid) { throw new IncorrectClaimException( String.format("The Token can't be used before %s.", claimVal), claimName, claim); @@ -356,10 +356,10 @@ private boolean assertValidInstantClaim(String claimName, Claim claim, long leew } private boolean assertInstantIsFuture(Instant claimVal, long leeway, Instant now) { - return !(claimVal != null && now.minus(Duration.ofSeconds(leeway)).isAfter(claimVal)); + return claimVal == null || now.minus(Duration.ofSeconds(leeway)).isBefore(claimVal); } - private boolean assertInstantIsPast(Instant claimVal, long leeway, Instant now) { + private boolean assertInstantIsLessThanOrEqualToNow(Instant claimVal, long leeway, Instant now) { return !(claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)); } diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index b9f56a2e..087f1e9e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -12,6 +12,7 @@ import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.util.Base64; @@ -270,12 +271,12 @@ public void shouldGetStringAudience() { @Test public void shouldGetExpirationTime() { long seconds = 1477592L; - Clock clock = Clock.fixed(Instant.ofEpochSecond(seconds), ZoneId.of("UTC")); + Clock mockNow = Clock.fixed(Instant.ofEpochSecond(seconds - 1), ZoneId.of("UTC")); String token = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0Nzc1OTJ9.x_ZjkPkKYUV5tdvc0l8go6D_z2kez1MQcOxokXrDc3k"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification - .build(clock) + .build(mockNow) .verify(token); assertThat(jwt, is(notNullValue())); diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 6d8ba201..5a784b87 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -657,6 +657,7 @@ public void shouldThrowOnNegativeCustomLeeway() { } // Expires At + @Test public void shouldValidateExpiresAtWithLeeway() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; @@ -674,12 +675,26 @@ public void shouldValidateExpiresAtIfPresent() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification - .build(mockNow) + .build(mockOneSecondEarlier) .verify(token); assertThat(jwt, is(notNullValue())); } + @Test + public void shouldThrowWhenExpiresAtIsNow() { + // exp must be > now + TokenExpiredException e = assertThrows(null, TokenExpiredException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + verification + .build(mockNow) + .verify(token); + }); + assertThat(e.getMessage(), is("The Token has expired on 1970-01-18T02:26:32Z.")); + assertThat(e.getExpiredOn(), is(Instant.ofEpochSecond(1477592L))); + } + @Test public void shouldThrowOnInvalidExpiresAtIfPresent() { TokenExpiredException e = assertThrows(null, TokenExpiredException.class, () -> { @@ -731,7 +746,18 @@ public void shouldThrowOnInvalidNotBeforeIfPresent() { @Test public void shouldValidateNotBeforeIfPresent() { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTN9.f4zVV0TbbTG5xxDjSoGZ320JIMchGoQCWrnT5MyQdT0"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .build(mockOneSecondLater) + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldAcceptNotBeforeEqualToNow() { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTJ9.71XBtRmkAa4iKnyhbS4NPW-Xr26eAVAdHZgmupS7a5o"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification .build(mockNow) From 902431804ee68a0e4727f41397cd56608918ba37 Mon Sep 17 00:00:00 2001 From: noetro Date: Tue, 31 Jan 2023 03:09:45 +0100 Subject: [PATCH 256/293] Improve JWT parse / decode performance (#620) * Optimise parsing of token for well-defined JWT format * Update error message in test to match new code * Fixing checkstyle issues * Added missing test case for no parts * Return new JWTDecodeException Return a new JWTDecodeException from private utility method `wrongNumberOfParts`, instead of throwing, since we throw from `splitToken()`. * Add JMH support to build script * Add benchmark for decoder and cleanup build file * Optimise JWT deserialisation by re-using threadsafe Jackson objects * Disable lint checks on JMH source set that is for testing * Remove extra line break --------- Co-authored-by: Jim Anderson Co-authored-by: Jim Anderson --- lib/build.gradle | 46 ++++++++++++++++++- .../jwt/benchmark/JWTDecoderBenchmark.java | 20 ++++++++ .../java/com/auth0/jwt/impl/BasicHeader.java | 13 +++--- .../auth0/jwt/impl/HeaderDeserializer.java | 20 +++----- .../java/com/auth0/jwt/impl/JWTParser.java | 22 +++++++-- .../com/auth0/jwt/impl/JsonNodeClaim.java | 36 ++++++++------- .../auth0/jwt/impl/PayloadDeserializer.java | 22 ++++----- .../java/com/auth0/jwt/impl/PayloadImpl.java | 20 ++++---- .../jwt/impl/HeaderDeserializerTest.java | 10 ++-- .../com/auth0/jwt/impl/JWTParserTest.java | 2 +- .../com/auth0/jwt/impl/JsonNodeClaimTest.java | 25 ++++++---- .../jwt/impl/PayloadDeserializerTest.java | 19 ++++---- .../com/auth0/jwt/impl/PayloadImplTest.java | 29 ++++++------ 13 files changed, 181 insertions(+), 103 deletions(-) create mode 100644 lib/src/jmh/java/com/auth0/jwt/benchmark/JWTDecoderBenchmark.java diff --git a/lib/build.gradle b/lib/build.gradle index 6b2fdfe3..6190a239 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -6,10 +6,28 @@ plugins { id 'checkstyle' } +sourceSets { + jmh { + + } +} + +configurations { + jmhImplementation { + extendsFrom implementation + } +} + checkstyle { toolVersion '10.0' - checkstyleTest.enabled = false //We are disabling lint checks for tests } +//We are disabling lint checks for tests +tasks.named("checkstyleTest").configure({ + enabled = false +}) +tasks.named("checkstyleJmh").configure({ + enabled = false +}) logger.lifecycle("Using version ${version} for ${group}.${name}") @@ -61,6 +79,10 @@ dependencies { testImplementation 'net.jodah:concurrentunit:0.4.6' testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.mockito:mockito-core:4.4.0' + + jmhImplementation sourceSets.main.output + jmhImplementation 'org.openjdk.jmh:jmh-core:1.35' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.35' } jacoco { @@ -143,3 +165,25 @@ task exportVersion() { new File(rootDir, "version.txt").text = "$version" } } + +// you can pass any arguments JMH accepts via Gradle args. +// Example: ./gradlew runJMH --args="-lrf" +tasks.register('runJMH', JavaExec) { + description 'Run JMH benchmarks.' + group 'verification' + + main 'org.openjdk.jmh.Main' + classpath sourceSets.jmh.runtimeClasspath + + args project.hasProperty("args") ? project.property("args").split() : "" +} +tasks.register('jmhHelp', JavaExec) { + description 'Prints the available command line options for JMH.' + group 'help' + + main 'org.openjdk.jmh.Main' + classpath sourceSets.jmh.runtimeClasspath + + args '-h' +} + diff --git a/lib/src/jmh/java/com/auth0/jwt/benchmark/JWTDecoderBenchmark.java b/lib/src/jmh/java/com/auth0/jwt/benchmark/JWTDecoderBenchmark.java new file mode 100644 index 00000000..81d3737a --- /dev/null +++ b/lib/src/jmh/java/com/auth0/jwt/benchmark/JWTDecoderBenchmark.java @@ -0,0 +1,20 @@ +package com.auth0.jwt.benchmark; + +import com.auth0.jwt.JWT; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.infra.Blackhole; + +/** + * This class is a JMH benchmark for decoding JWTs. + */ +public class JWTDecoderBenchmark { + private static final String TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + + @Benchmark + @BenchmarkMode(Mode.Throughput) + public void throughputDecodeTime(Blackhole blackhole) { + blackhole.consume(JWT.decode(TOKEN)); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java index 8031d8c0..5a881ab5 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java +++ b/lib/src/main/java/com/auth0/jwt/impl/BasicHeader.java @@ -2,12 +2,11 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Header; +import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectReader; import java.io.Serializable; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim; @@ -23,7 +22,7 @@ class BasicHeader implements Header, Serializable { private final String contentType; private final String keyId; private final Map tree; - private final ObjectReader objectReader; + private final ObjectCodec objectCodec; BasicHeader( String algorithm, @@ -31,14 +30,14 @@ class BasicHeader implements Header, Serializable { String contentType, String keyId, Map tree, - ObjectReader objectReader + ObjectCodec objectCodec ) { this.algorithm = algorithm; this.type = type; this.contentType = contentType; this.keyId = keyId; - this.tree = Collections.unmodifiableMap(tree == null ? new HashMap<>() : tree); - this.objectReader = objectReader; + this.tree = tree == null ? Collections.emptyMap() : Collections.unmodifiableMap(tree); + this.objectCodec = objectCodec; } Map getTree() { @@ -67,6 +66,6 @@ public String getKeyId() { @Override public Claim getHeaderClaim(String name) { - return extractClaim(name, tree, objectReader); + return extractClaim(name, tree, objectCodec); } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java index 9293fd4d..ad6e4ce0 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -2,11 +2,11 @@ import com.auth0.jwt.HeaderParams; import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.Header; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; @@ -19,22 +19,14 @@ * * @see JWTParser */ -class HeaderDeserializer extends StdDeserializer { +class HeaderDeserializer extends StdDeserializer
{ - private final ObjectReader objectReader; - - HeaderDeserializer(ObjectReader objectReader) { - this(null, objectReader); - } - - private HeaderDeserializer(Class vc, ObjectReader objectReader) { - super(vc); - - this.objectReader = objectReader; + HeaderDeserializer() { + super(Header.class); } @Override - public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + public Header deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { Map tree = p.getCodec().readValue(p, new TypeReference>() { }); if (tree == null) { @@ -45,7 +37,7 @@ public BasicHeader deserialize(JsonParser p, DeserializationContext ctxt) throws String type = getString(tree, HeaderParams.TYPE); String contentType = getString(tree, HeaderParams.CONTENT_TYPE); String keyId = getString(tree, HeaderParams.KEY_ID); - return new BasicHeader(algorithm, type, contentType, keyId, tree, objectReader); + return new BasicHeader(algorithm, type, contentType, keyId, tree, p.getCodec()); } String getString(Map tree, String claimName) { diff --git a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java index fe1600bd..022520f5 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java @@ -16,15 +16,21 @@ * {@link HeaderSerializer} and {@link PayloadSerializer}. */ public class JWTParser implements JWTPartsParser { + private static final ObjectMapper DEFAULT_OBJECT_MAPPER = createDefaultObjectMapper(); + private static final ObjectReader DEFAULT_PAYLOAD_READER = DEFAULT_OBJECT_MAPPER.readerFor(Payload.class); + private static final ObjectReader DEFAULT_HEADER_READER = DEFAULT_OBJECT_MAPPER.readerFor(Header.class); + private final ObjectReader payloadReader; private final ObjectReader headerReader; public JWTParser() { - this(getDefaultObjectMapper()); + this.payloadReader = DEFAULT_PAYLOAD_READER; + this.headerReader = DEFAULT_HEADER_READER; } JWTParser(ObjectMapper mapper) { addDeserializers(mapper); + this.payloadReader = mapper.readerFor(Payload.class); this.headerReader = mapper.readerFor(Header.class); } @@ -55,18 +61,24 @@ public Header parseHeader(String json) throws JWTDecodeException { } } - private void addDeserializers(ObjectMapper mapper) { + static void addDeserializers(ObjectMapper mapper) { SimpleModule module = new SimpleModule(); - ObjectReader reader = mapper.reader(); - module.addDeserializer(Payload.class, new PayloadDeserializer(reader)); - module.addDeserializer(Header.class, new HeaderDeserializer(reader)); + module.addDeserializer(Payload.class, new PayloadDeserializer()); + module.addDeserializer(Header.class, new HeaderDeserializer()); mapper.registerModule(module); } static ObjectMapper getDefaultObjectMapper() { + return DEFAULT_OBJECT_MAPPER; + } + + private static ObjectMapper createDefaultObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); + + addDeserializers(mapper); + return mapper; } diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 456d1515..0a7e22f3 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -4,9 +4,9 @@ import com.auth0.jwt.interfaces.Claim; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectReader; import java.io.IOException; import java.lang.reflect.Array; @@ -21,12 +21,12 @@ */ class JsonNodeClaim implements Claim { - private final ObjectReader objectReader; + private final ObjectCodec codec; private final JsonNode data; - private JsonNodeClaim(JsonNode node, ObjectReader objectReader) { + private JsonNodeClaim(JsonNode node, ObjectCodec codec) { this.data = node; - this.objectReader = objectReader; + this.codec = codec; } @Override @@ -82,7 +82,7 @@ public T[] asArray(Class clazz) throws JWTDecodeException { T[] arr = (T[]) Array.newInstance(clazz, data.size()); for (int i = 0; i < data.size(); i++) { try { - arr[i] = objectReader.treeToValue(data.get(i), clazz); + arr[i] = codec.treeToValue(data.get(i), clazz); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e); } @@ -99,7 +99,7 @@ public List asList(Class clazz) throws JWTDecodeException { List list = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { try { - list.add(objectReader.treeToValue(data.get(i), clazz)); + list.add(codec.treeToValue(data.get(i), clazz)); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to " + clazz.getSimpleName(), e); } @@ -113,11 +113,11 @@ public Map asMap() throws JWTDecodeException { return null; } - try { - TypeReference> mapType = new TypeReference>() { - }; - JsonParser thisParser = objectReader.treeAsTokens(data); - return thisParser.readValueAs(mapType); + TypeReference> mapType = new TypeReference>() { + }; + + try (JsonParser parser = codec.treeAsTokens(data)) { + return parser.readValueAs(mapType); } catch (IOException e) { throw new JWTDecodeException("Couldn't map the Claim value to Map", e); } @@ -129,8 +129,8 @@ public T as(Class clazz) throws JWTDecodeException { if (isMissing() || isNull()) { return null; } - return objectReader.treeAsTokens(data).readValueAs(clazz); - } catch (IOException e) { + return codec.treeToValue(data, clazz); + } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e); } } @@ -160,21 +160,23 @@ public String toString() { * * @param claimName the Claim to search for. * @param tree the JsonNode tree to search the Claim in. + * @param objectCodec the object codec in use for deserialization * @return a valid non-null Claim. */ - static Claim extractClaim(String claimName, Map tree, ObjectReader objectReader) { + static Claim extractClaim(String claimName, Map tree, ObjectCodec objectCodec) { JsonNode node = tree.get(claimName); - return claimFromNode(node, objectReader); + return claimFromNode(node, objectCodec); } /** * Helper method to create a Claim representation from the given JsonNode. * * @param node the JsonNode to convert into a Claim. + * @param objectCodec the object codec in use for deserialization * @return a valid Claim instance. If the node is null or missing, a NullClaim will be returned. */ - static Claim claimFromNode(JsonNode node, ObjectReader objectReader) { - return new JsonNodeClaim(node, objectReader); + static Claim claimFromNode(JsonNode node, ObjectCodec objectCodec) { + return new JsonNodeClaim(node, objectCodec); } } \ No newline at end of file diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 37e70f7a..65fba3ac 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -5,6 +5,7 @@ import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; @@ -24,16 +25,8 @@ */ class PayloadDeserializer extends StdDeserializer { - private final ObjectReader objectReader; - - PayloadDeserializer(ObjectReader reader) { - this(null, reader); - } - - private PayloadDeserializer(Class vc, ObjectReader reader) { - super(vc); - - this.objectReader = reader; + PayloadDeserializer() { + super(Payload.class); } @Override @@ -46,16 +39,17 @@ public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOE String issuer = getString(tree, RegisteredClaims.ISSUER); String subject = getString(tree, RegisteredClaims.SUBJECT); - List audience = getStringOrArray(tree, RegisteredClaims.AUDIENCE); + List audience = getStringOrArray(p.getCodec(), tree, RegisteredClaims.AUDIENCE); Instant expiresAt = getInstantFromSeconds(tree, RegisteredClaims.EXPIRES_AT); Instant notBefore = getInstantFromSeconds(tree, RegisteredClaims.NOT_BEFORE); Instant issuedAt = getInstantFromSeconds(tree, RegisteredClaims.ISSUED_AT); String jwtId = getString(tree, RegisteredClaims.JWT_ID); - return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, objectReader); + return new PayloadImpl(issuer, subject, audience, expiresAt, notBefore, issuedAt, jwtId, tree, p.getCodec()); } - List getStringOrArray(Map tree, String claimName) throws JWTDecodeException { + List getStringOrArray(ObjectCodec codec, Map tree, String claimName) + throws JWTDecodeException { JsonNode node = tree.get(claimName); if (node == null || node.isNull() || !(node.isArray() || node.isTextual())) { return null; @@ -67,7 +61,7 @@ List getStringOrArray(Map tree, String claimName) thro List list = new ArrayList<>(node.size()); for (int i = 0; i < node.size(); i++) { try { - list.add(objectReader.treeToValue(node.get(i), String.class)); + list.add(codec.treeToValue(node.get(i), String.class)); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to String", e); } diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java index 75e79474..bfd9b0ea 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadImpl.java @@ -2,12 +2,16 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.Payload; +import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectReader; import java.io.Serializable; import java.time.Instant; -import java.util.*; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim; @@ -30,7 +34,7 @@ class PayloadImpl implements Payload, Serializable { private final Instant issuedAt; private final String jwtId; private final Map tree; - private final ObjectReader objectReader; + private final ObjectCodec objectCodec; PayloadImpl( String issuer, @@ -41,7 +45,7 @@ class PayloadImpl implements Payload, Serializable { Instant issuedAt, String jwtId, Map tree, - ObjectReader objectReader + ObjectCodec objectCodec ) { this.issuer = issuer; this.subject = subject; @@ -50,8 +54,8 @@ class PayloadImpl implements Payload, Serializable { this.notBefore = notBefore; this.issuedAt = issuedAt; this.jwtId = jwtId; - this.tree = tree != null ? Collections.unmodifiableMap(tree) : Collections.emptyMap(); - this.objectReader = objectReader; + this.tree = tree != null ? Collections.unmodifiableMap(tree) : Collections.emptyMap(); + this.objectCodec = objectCodec; } Map getTree() { @@ -111,14 +115,14 @@ public String getId() { @Override public Claim getClaim(String name) { - return extractClaim(name, tree, objectReader); + return extractClaim(name, tree, objectCodec); } @Override public Map getClaims() { Map claims = new HashMap<>(tree.size() * 2); for (String name : tree.keySet()) { - claims.put(name, extractClaim(name, tree, objectReader)); + claims.put(name, extractClaim(name, tree, objectCodec)); } return Collections.unmodifiableMap(claims); } diff --git a/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java index 328f4ab4..02d782a7 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.TextNode; import org.junit.Before; @@ -22,8 +21,10 @@ import java.util.HashMap; import java.util.Map; -import static org.hamcrest.Matchers.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -34,11 +35,10 @@ public class HeaderDeserializerTest { @Rule public ExpectedException exception = ExpectedException.none(); private HeaderDeserializer deserializer; - private ObjectReader objectReader = new ObjectMapper().reader(); @Before public void setUp() { - deserializer = new HeaderDeserializer(objectReader); + deserializer = new HeaderDeserializer(); } @Test @@ -46,7 +46,7 @@ public void shouldThrowOnNullTree() throws Exception { exception.expect(JWTDecodeException.class); exception.expectMessage("Parsing the Header's JSON resulted on a Null map"); - JsonDeserializer deserializer = new HeaderDeserializer(objectReader); + JsonDeserializer deserializer = new HeaderDeserializer(); JsonParser parser = mock(JsonParser.class); ObjectCodec codec = mock(ObjectCodec.class); DeserializationContext context = mock(DeserializationContext.class); diff --git a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java index a40a23d4..da62131a 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java @@ -42,7 +42,7 @@ public void shouldGetDefaultObjectMapper() { @Test public void shouldAddDeserializers() { ObjectMapper mapper = mock(ObjectMapper.class); - new JWTParser(mapper); + JWTParser.addDeserializers(mapper); verify(mapper).registerModule(any(Module.class)); } diff --git a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java index 18f59f04..a0364953 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.MissingNode; import com.fasterxml.jackson.databind.node.NullNode; @@ -21,20 +20,31 @@ import java.io.IOException; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsNull.notNullValue; import static org.hamcrest.core.IsNull.nullValue; import static org.junit.Assert.assertNull; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; public class JsonNodeClaimTest { private ObjectMapper mapper; - private ObjectReader objectReader; @Rule public ExpectedException exception = ExpectedException.none(); @@ -42,7 +52,6 @@ public class JsonNodeClaimTest { @Before public void setUp() { mapper = getDefaultObjectMapper(); - objectReader = mapper.reader(); } @Test @@ -55,7 +64,7 @@ public void shouldGetBooleanValue() { } private Claim claimFromNode(JsonNode value) { - return JsonNodeClaim.claimFromNode(value, objectReader); + return JsonNodeClaim.claimFromNode(value, mapper); } @Test @@ -282,7 +291,7 @@ public void shouldThrowIfAnExtraordinaryExceptionHappensWhenParsingAsGenericMap( JsonNode value = mock(ObjectNode.class); when(value.getNodeType()).thenReturn(JsonNodeType.OBJECT); - ObjectReader mockedMapper = mock(ObjectReader.class); + ObjectMapper mockedMapper = mock(ObjectMapper.class); JsonNodeClaim claim = (JsonNodeClaim) JsonNodeClaim.claimFromNode(value, mockedMapper); JsonNodeClaim spiedClaim = spy(claim); diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java index 2d80bed9..86c4f10f 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -35,9 +35,12 @@ public class PayloadDeserializerTest { public ExpectedException exception = ExpectedException.none(); private PayloadDeserializer deserializer; + private ObjectMapper objectMapper; + @Before public void setUp() { - deserializer = new PayloadDeserializer(new ObjectMapper().reader()); + objectMapper = new ObjectMapper(); + deserializer = new PayloadDeserializer(); } @Test @@ -68,7 +71,7 @@ public void shouldThrowWhenParsingArrayWithObjectValue() throws Exception { ArrayNode arrNode = new ArrayNode(JsonNodeFactory.instance, subNodes); tree.put("key", arrNode); - deserializer.getStringOrArray(tree, "key"); + deserializer.getStringOrArray(objectMapper, tree, "key"); } @Test @@ -123,7 +126,7 @@ public void shouldGetStringArrayWhenParsingArrayNode() { ArrayNode arrNode = new ArrayNode(JsonNodeFactory.instance, subNodes); tree.put("key", arrNode); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(notNullValue())); assertThat(values, is(IsCollectionWithSize.hasSize(2))); assertThat(values, is(IsIterableContaining.hasItems("one", "two"))); @@ -135,7 +138,7 @@ public void shouldGetStringArrayWhenParsingTextNode() { TextNode textNode = new TextNode("something"); tree.put("key", textNode); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(notNullValue())); assertThat(values, is(IsCollectionWithSize.hasSize(1))); assertThat(values, is(IsIterableContaining.hasItems("something"))); @@ -147,7 +150,7 @@ public void shouldGetEmptyStringArrayWhenParsingEmptyTextNode() { TextNode textNode = new TextNode(""); tree.put("key", textNode); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(notNullValue())); assertThat(values, is(IsEmptyCollection.empty())); } @@ -158,7 +161,7 @@ public void shouldGetNullArrayWhenParsingNullNode() { NullNode node = NullNode.getInstance(); tree.put("key", node); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(nullValue())); } @@ -167,7 +170,7 @@ public void shouldGetNullArrayWhenParsingNullNodeValue() { Map tree = new HashMap<>(); tree.put("key", null); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(nullValue())); } @@ -177,7 +180,7 @@ public void shouldGetNullArrayWhenParsingNonArrayOrTextNode() { IntNode node = new IntNode(456789); tree.put("key", node); - List values = deserializer.getStringOrArray(tree, "key"); + List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(nullValue())); } diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java index da0c880e..5ad7ac68 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -29,29 +29,28 @@ public class PayloadImplTest { private final Instant notBefore = Instant.now(); private final Instant issuedAt = Instant.now(); - private ObjectReader objectReader; + private ObjectMapper objectMapper; @Before public void setUp() { - ObjectMapper mapper = getDefaultObjectMapper(); - objectReader = mapper.reader(); + objectMapper = getDefaultObjectMapper(); Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); - payload = new PayloadImpl("issuer", "subject", Collections.singletonList("audience"), expiresAt, notBefore, issuedAt, "jwtId", tree, objectReader); + payload = new PayloadImpl("issuer", "subject", Collections.singletonList("audience"), expiresAt, notBefore, issuedAt, "jwtId", tree, objectMapper); } @Test public void shouldHaveUnmodifiableTree() { exception.expect(UnsupportedOperationException.class); - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, new HashMap<>(), objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, new HashMap<>(), objectMapper); payload.getTree().put("something", null); } @Test public void shouldHaveUnmodifiableAudience() { exception.expect(UnsupportedOperationException.class); - PayloadImpl payload = new PayloadImpl(null, null, new ArrayList<>(), null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, new ArrayList<>(), null, null, null, null, null, objectMapper); payload.getAudience().add("something"); } @@ -63,7 +62,7 @@ public void shouldGetIssuer() { @Test public void shouldGetNullIssuerIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getIssuer(), is(nullValue())); } @@ -76,7 +75,7 @@ public void shouldGetSubject() { @Test public void shouldGetNullSubjectIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getSubject(), is(nullValue())); } @@ -91,7 +90,7 @@ public void shouldGetAudience() { @Test public void shouldGetNullAudienceIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getAudience(), is(nullValue())); } @@ -105,7 +104,7 @@ public void shouldGetExpiresAt() { @Test public void shouldGetNullExpiresAtIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getExpiresAt(), is(nullValue())); assertThat(payload.getExpiresAtAsInstant(), is(nullValue())); @@ -120,7 +119,7 @@ public void shouldGetNotBefore() { @Test public void shouldGetNullNotBeforeIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getNotBefore(), is(nullValue())); assertThat(payload.getNotBeforeAsInstant(), is(nullValue())); @@ -135,7 +134,7 @@ public void shouldGetIssuedAt() { @Test public void shouldGetNullIssuedAtIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getIssuedAt(), is(nullValue())); assertThat(payload.getIssuedAtAsInstant(), is(nullValue())); @@ -149,7 +148,7 @@ public void shouldGetJWTId() { @Test public void shouldGetNullJWTIdIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getId(), is(nullValue())); } @@ -163,7 +162,7 @@ public void shouldGetExtraClaim() { @Test public void shouldGetNotNullExtraClaimIfMissing() { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getClaim("missing"), is(notNullValue())); assertThat(payload.getClaim("missing").isMissing(), is(true)); @@ -175,7 +174,7 @@ public void shouldGetClaims() { Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); tree.put("sub", new TextNode("auth0")); - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, tree, objectReader); + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, tree, objectMapper); assertThat(payload, is(notNullValue())); Map claims = payload.getClaims(); assertThat(claims, is(notNullValue())); From b610b6635ba1ed948a06fea777eeeecca2364303 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Sun, 12 Feb 2023 21:53:23 -0600 Subject: [PATCH 257/293] Release 4.3.0 (#655) --- CHANGELOG.md | 10 ++++++++++ README.md | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eaf3feec..61ebad87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## [4.3.0](https://github.com/auth0/java-jwt/tree/4.3.0) (2023-02-10) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.2...4.3.0) + +**Changed** +- Improve JWT parse/decode performance [\#620](https://github.com/auth0/java-jwt/pull/620) ([noetro](https://github.com/noetro)) + +**Fixed** +- Fix for exp claim considered valid if equal to now [\#652](https://github.com/auth0/java-jwt/pull/652) ([jimmyjames](https://github.com/jimmyjames)) +- Code cleanup [\#642](https://github.com/auth0/java-jwt/pull/642) ([CodeDead](https://github.com/CodeDead)) + ## [4.2.2](https://github.com/auth0/java-jwt/tree/4.2.2) (2023-01-11) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.1...4.2.2) diff --git a/README.md b/README.md index 8583ed7a..9391f87d 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ Add the dependency via Maven: com.auth0 java-jwt - 4.2.2 + 4.3.0 ``` or Gradle: ```gradle -implementation 'com.auth0:java-jwt:4.2.2' +implementation 'com.auth0:java-jwt:4.3.0' ``` ### Create a JWT From a18955bb99bf6994e424971db3f621bc0cc64a86 Mon Sep 17 00:00:00 2001 From: Andreas Rigas <48296471+andrewrigas@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:27:30 +0100 Subject: [PATCH 258/293] Add support for passing json values for header and payload (#643) --- lib/build.gradle | 2 +- .../main/java/com/auth0/jwt/JWTCreator.java | 48 ++++++++++++ .../java/com/auth0/jwt/JWTCreatorTest.java | 78 ++++++++++++++++++- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 6190a239..d147f2d0 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -72,7 +72,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 7ed83940..f554cbc3 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -98,6 +98,27 @@ public Builder withHeader(Map headerClaims) { return this; } + /** + * Add specific Claims to set as the Header. + * If provided json is null then nothing is changed + * + * @param headerClaimsJson the values to use as Claims in the token's Header. + * @return this same Builder instance. + * @throws IllegalArgumentException if json value has invalid structure + */ + public Builder withHeader(String headerClaimsJson) throws IllegalArgumentException { + if (headerClaimsJson == null) { + return this; + } + + try { + Map headerClaims = mapper.readValue(headerClaimsJson, HashMap.class); + return withHeader(headerClaims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid header JSON", e); + } + } + /** * Add a specific Key Id ("kid") claim to the Header. * If the {@link Algorithm} used to sign this token was instantiated with a KeyProvider, @@ -467,6 +488,33 @@ public Builder withPayload(Map payloadClaims) throws IllegalArgumentE return this; } + /** + * Add specific Claims to set as the Payload. If the provided json is null then + * nothing is changed. + * + *

+ * If any of the claims are invalid, none will be added. + *

+ * + * @param payloadClaimsJson the values to use as Claims in the token's payload. + * @return this same Builder instance. + * @throws IllegalArgumentException if any of the claim keys or null, + * or if the values are not of a supported type, + * or if json value has invalid structure. + */ + public Builder withPayload(String payloadClaimsJson) throws IllegalArgumentException { + if (payloadClaimsJson == null) { + return this; + } + + try { + Map payloadClaims = mapper.readValue(payloadClaimsJson, HashMap.class); + return withPayload(payloadClaims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid payload JSON", e); + } + } + private boolean validatePayload(Map payload) { for (Map.Entry entry : payload.entrySet()) { String key = entry.getKey(); diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index 8fc83b44..e4ab8cb0 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -3,6 +3,7 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; @@ -82,13 +83,48 @@ public void shouldAddHeaderClaim() { @Test public void shouldReturnBuilderIfNullMapIsProvided() { + Map nullMap = null; + String nullString = null; String signed = JWTCreator.init() - .withHeader(null) + .withHeader(nullMap) + .withHeader(nullString) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); } + @Test + public void shouldSupportJsonValueHeaderWithNestedDataStructure() { + String stringClaim = "someClaim"; + Integer intClaim = 1; + List nestedListClaims = Arrays.asList("1", "2"); + String claimsJson = "{\"stringClaim\": \"someClaim\", \"intClaim\": 1, \"nestedClaim\": { \"listClaim\": [ \"1\", \"2\" ]}}"; + + String jwt = JWTCreator.init() + .withHeader(claimsJson) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + + assertThat(headerJson, JsonMatcher.hasEntry("stringClaim", stringClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("intClaim", intClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("listClaim", nestedListClaims)); + } + + @Test + public void shouldFailWithIllegalArgumentExceptionForInvalidJsonForHeaderClaims() { + String invalidJson = "{ invalidJson }"; + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid header JSON"); + + JWTCreator.init() + .withHeader(invalidJson) + .sign(Algorithm.HMAC256("secret")); + } + @Test public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() { Map header = new HashMap<>(); @@ -105,6 +141,7 @@ public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() { assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } + @Test public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() { Map header = new HashMap<>(); @@ -715,8 +752,11 @@ public void withPayloadShouldAddBasicClaim() { @Test public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() { + Map nullMap = null; + String nullString = null; String jwt = JWTCreator.init() - .withPayload(null) + .withPayload(nullMap) + .withPayload(nullString) .sign(Algorithm.HMAC256("secret")); assertThat(jwt, is(notNullValue())); @@ -921,10 +961,42 @@ public void withPayloadShouldSupportNullValuesEverywhere() { assertThat(headerJson, JsonMatcher.hasEntry("objClaim", objClaim)); } + @Test + public void withPayloadShouldSupportJsonValueWithNestedDataStructure() { + String stringClaim = "someClaim"; + Integer intClaim = 1; + List nestedListClaims = Arrays.asList("1", "2"); + String claimsJson = "{\"stringClaim\": \"someClaim\", \"intClaim\": 1, \"nestedClaim\": { \"listClaim\": [ \"1\", \"2\" ]}}"; + + String jwt = JWTCreator.init() + .withPayload(claimsJson) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + + assertThat(payloadJson, JsonMatcher.hasEntry("stringClaim", stringClaim)); + assertThat(payloadJson, JsonMatcher.hasEntry("intClaim", intClaim)); + assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", nestedListClaims)); + } + + @Test + public void shouldFailWithIllegalArgumentExceptionForInvalidJsonForPayloadClaims() { + String invalidJson = "{ invalidJson }"; + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid payload JSON"); + + JWTCreator.init() + .withPayload(invalidJson) + .sign(Algorithm.HMAC256("secret")); + } + @Test public void shouldCreatePayloadWithNullForMap() { String jwt = JWTCreator.init() - .withClaim("name", (Map) null) + .withClaim("name", (Map) null) .sign(Algorithm.HMAC256("secret")); assertThat(jwt, is(notNullValue())); assertTrue(JWT.decode(jwt).getClaim("name").isNull()); From e85a00a00543b2b17d0460e9c1c11c2aa31fbcbc Mon Sep 17 00:00:00 2001 From: Robin Karlsson Date: Mon, 27 Mar 2023 21:57:04 +0200 Subject: [PATCH 259/293] Preserve insertion order for claims (#656) Co-authored-by: Jim Anderson --- .../main/java/com/auth0/jwt/JWTCreator.java | 8 +-- .../java/com/auth0/jwt/JWTCreatorTest.java | 49 ++++++++++++++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index f554cbc3..0b0d21e4 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -71,8 +71,8 @@ public static class Builder { private final Map headerClaims; Builder() { - this.payloadClaims = new HashMap<>(); - this.headerClaims = new HashMap<>(); + this.payloadClaims = new LinkedHashMap<>(); + this.headerClaims = new LinkedHashMap<>(); } /** @@ -112,7 +112,7 @@ public Builder withHeader(String headerClaimsJson) throws IllegalArgumentExcepti } try { - Map headerClaims = mapper.readValue(headerClaimsJson, HashMap.class); + Map headerClaims = mapper.readValue(headerClaimsJson, LinkedHashMap.class); return withHeader(headerClaims); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Invalid header JSON", e); @@ -508,7 +508,7 @@ public Builder withPayload(String payloadClaimsJson) throws IllegalArgumentExcep } try { - Map payloadClaims = mapper.readValue(payloadClaimsJson, HashMap.class); + Map payloadClaims = mapper.readValue(payloadClaimsJson, LinkedHashMap.class); return withPayload(payloadClaims); } catch (JsonProcessingException e) { throw new IllegalArgumentException("Invalid payload JSON", e); diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index e4ab8cb0..53cd267b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -3,8 +3,8 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -1010,4 +1010,51 @@ public void shouldCreatePayloadWithNullForList() { assertThat(jwt, is(notNullValue())); assertTrue(JWT.decode(jwt).getClaim("name").isNull()); } + + @Test + public void shouldPreserveInsertionOrder() throws Exception { + String taxonomyJson = "{\"class\": \"mammalia\", \"order\": \"carnivora\", \"family\": \"canidae\", \"genus\": \"vulpes\"}"; + List taxonomyClaims = Arrays.asList("class", "order", "family", "genus"); + List headerInsertionOrder = new ArrayList<>(taxonomyClaims); + Map header = new LinkedHashMap<>(); + for (int i = 0; i < 10; i++) { + String key = "h" + i; + header.put(key, "v" + 1); + headerInsertionOrder.add(key); + } + + List payloadInsertionOrder = new ArrayList<>(taxonomyClaims); + JWTCreator.Builder builder = JWTCreator.init() + .withHeader(taxonomyJson) + .withHeader(header) + .withPayload(taxonomyJson); + for (int i = 0; i < 10; i++) { + String name = "c" + i; + builder = builder.withClaim(name, "v" + i); + payloadInsertionOrder.add(name); + } + String signed = builder.sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + Base64.Decoder urlDecoder = Base64.getUrlDecoder(); + String headerJson = new String(urlDecoder.decode(parts[0]), StandardCharsets.UTF_8); + String payloadJson = new String(urlDecoder.decode(parts[1]), StandardCharsets.UTF_8); + + ObjectMapper objectMapper = new ObjectMapper(); + + List headerFields = new ArrayList<>(); + objectMapper.readValue(headerJson, ObjectNode.class) + .fieldNames().forEachRemaining(headerFields::add); + headerFields.retainAll(headerInsertionOrder); + assertThat("Header insertion order should be preserved", + headerFields, is(equalTo(headerInsertionOrder))); + + List payloadFields = new ArrayList<>(); + objectMapper.readValue(payloadJson, ObjectNode.class) + .fieldNames().forEachRemaining(payloadFields::add); + payloadFields.retainAll(payloadInsertionOrder); + assertThat("Claim insertion order should be preserved", + payloadFields, is(equalTo(payloadInsertionOrder))); + } } From 652bf7dac4c10d1213609a2db6fb50815920ac09 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 30 Mar 2023 22:12:01 -0500 Subject: [PATCH 260/293] update jackson to 2.14.2 (#657) --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index d147f2d0..c4e11764 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -72,7 +72,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' From 82548a6e7b69ad0c6974519c9ebb313d15237cad Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 31 Mar 2023 13:31:38 -0500 Subject: [PATCH 261/293] Release 4.4.0 (#658) --- CHANGELOG.md | 8 ++++++++ README.md | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ebad87..1b1e75bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [4.4.0](https://github.com/auth0/java-jwt/tree/4.4.0) (2023-03-31) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.3.0...4.4.0) + +**Changed** +- Add support for passing json values for header and payload [\#643](https://github.com/auth0/java-jwt/pull/643) ([andrewrigas](https://github.com/andrewrigas)) +- Preserve insertion order for claims [\#656](https://github.com/auth0/java-jwt/pull/656) ([snago](https://github.com/snago)) +- Update Jackson to 2.14.2 [\#657](https://github.com/auth0/java-jwt/pull/657) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.3.0](https://github.com/auth0/java-jwt/tree/4.3.0) (2023-02-10) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.2.2...4.3.0) diff --git a/README.md b/README.md index 9391f87d..9f425898 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ Add the dependency via Maven: com.auth0 java-jwt - 4.3.0 + 4.4.0 ``` or Gradle: ```gradle -implementation 'com.auth0:java-jwt:4.3.0' +implementation 'com.auth0:java-jwt:4.4.0' ``` ### Create a JWT From 923e9c46db888c500ff44032fa900b871fae30ed Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 25 Apr 2023 23:02:41 -0300 Subject: [PATCH 262/293] Replace issue templates with issue forms [SDK-4167] (#661) --- .github/ISSUE_TEMPLATE/Bug Report.yml | 67 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/Feature Request.yml | 53 +++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 7 +-- .github/ISSUE_TEMPLATE/feature_request.md | 39 ------------- .github/ISSUE_TEMPLATE/report-a-bug.md | 55 ------------------ 5 files changed, 122 insertions(+), 99 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/Bug Report.yml create mode 100644 .github/ISSUE_TEMPLATE/Feature Request.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/ISSUE_TEMPLATE/report-a-bug.md diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml new file mode 100644 index 00000000..d5d861e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -0,0 +1,67 @@ +name: 🐞 Report a bug +description: Have you found a bug or issue? Create a bug report for this library +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have looked into the [Readme](https://github.com/auth0/java-jwt#readme) and [Examples](https://github.com/auth0/java-jwt/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. + required: true + - label: I have looked into the [API documentation](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) and have not found a suitable solution or answer. + required: true + - label: I have searched the [issues](https://github.com/auth0/java-jwt/issues) and have not found a suitable solution or answer. + required: true + - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. + required: true + - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). + required: true + + - type: textarea + id: description + attributes: + label: Description + description: Provide a clear and concise description of the issue, including what you expected to happen. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. + placeholder: | + 1. Step 1... + 2. Step 2... + 3. ... + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Other libraries that might be involved, or any other relevant information you think would be useful. + validations: + required: false + + - type: input + id: environment-version + attributes: + label: java-jwt version + validations: + required: true + + - type: input + id: environment-java-version + attributes: + label: Java version + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/Feature Request.yml b/.github/ISSUE_TEMPLATE/Feature Request.yml new file mode 100644 index 00000000..38fee433 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature Request.yml @@ -0,0 +1,53 @@ +name: 🧩 Feature request +description: Suggest an idea or a feature for this library +labels: ["feature request"] + +body: + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have looked into the [Readme](https://github.com/auth0/java-jwt#readme) and [Examples](https://github.com/auth0/java-jwt/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. + required: true + - label: I have looked into the [API documentation](https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html) and have not found a suitable solution or answer. + required: true + - label: I have searched the [issues](https://github.com/auth0/java-jwt/issues) and have not found a suitable solution or answer. + required: true + - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. + required: true + - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). + required: true + + - type: textarea + id: description + attributes: + label: Describe the problem you'd like to have solved + description: A clear and concise description of what the problem is. + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: ideal-solution + attributes: + label: Describe the ideal solution + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives-and-workarounds + attributes: + label: Alternatives and current workarounds + description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3cd7aa53..f58e0249 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,5 @@ blank_issues_enabled: false contact_links: - name: Auth0 Community - url: https://community.auth0.com/c/sdks/5 - about: Discuss this SDK in the Auth0 Community forums - - name: Library Documentation - url: https://github.com/auth0/java-jwt/blob/master/README.md - about: Read the library documentation + url: https://community.auth0.com + about: Discuss this library in the Auth0 Community forums diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 68352ba2..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: Feature request -about: Suggest an idea or a feature for this project -title: '' -labels: feature request -assignees: '' ---- - - - -### Describe the problem you'd like to have solved - - - -### Describe the ideal solution - - - -## Alternatives and current work-arounds - - - -### Additional information, if any - - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/report-a-bug.md b/.github/ISSUE_TEMPLATE/report-a-bug.md deleted file mode 100644 index e5cf8c46..00000000 --- a/.github/ISSUE_TEMPLATE/report-a-bug.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -name: Report a bug -about: Have you found a bug or issue? Create a bug report for this SDK -title: '' -labels: bug report -assignees: '' ---- - - - -### Describe the problem - - - -### What was the expected behavior? - - - -### Reproduction - - -- Step 1.. -- Step 2.. -- ... - -### Environment - - - -- **Version of this library used:** -- **Version of Java used:** -- **Other modules/plugins/libraries that might be involved:** -- **Any other relevant information you think would be useful:** From d8fe9a2654a69ff9c39c1fa711b8abb622a4f7d6 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 8 Jun 2023 21:15:57 -0500 Subject: [PATCH 263/293] Empty string audience claim should be deserialized as empty string (#663) --- lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java | 2 +- .../test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java index 65fba3ac..b1d32a12 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -54,7 +54,7 @@ List getStringOrArray(ObjectCodec codec, Map tree, Str if (node == null || node.isNull() || !(node.isArray() || node.isTextual())) { return null; } - if (node.isTextual() && !node.asText().isEmpty()) { + if (node.isTextual()) { return Collections.singletonList(node.asText()); } diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java index 86c4f10f..c3e04013 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -145,14 +145,14 @@ public void shouldGetStringArrayWhenParsingTextNode() { } @Test - public void shouldGetEmptyStringArrayWhenParsingEmptyTextNode() { + public void shouldGetEmptyStringInArrayWhenParsingEmptyTextNode() { Map tree = new HashMap<>(); TextNode textNode = new TextNode(""); tree.put("key", textNode); List values = deserializer.getStringOrArray(objectMapper, tree, "key"); assertThat(values, is(notNullValue())); - assertThat(values, is(IsEmptyCollection.empty())); + assertThat(values, is(IsIterableContaining.hasItem(""))); } @Test From 6ef84cf5a11dfdd45f6f00eb067f27123aa68bf7 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 17 Jul 2023 12:46:01 -0500 Subject: [PATCH 264/293] chore(security): Update and pin Graddle workflow actions --- .github/workflows/gradle-wrapper-validation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index a015578a..20c61e51 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,5 +6,5 @@ jobs: name: "validation/gradlew" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # pin@v1 From 8c9bbf8d838e6fed8ab589106a47eeb0b381ef28 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 17 Jul 2023 12:46:59 -0500 Subject: [PATCH 265/293] Update gradle-wrapper-validation.yml --- .github/workflows/gradle-wrapper-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 20c61e51..ce302cb4 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -7,4 +7,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # pin@v1 + - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # pin@v1.0.6 From ef1825b63c81cad6aa5b1e1b7c943ad639bd3140 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 17 Aug 2023 07:09:19 -0500 Subject: [PATCH 266/293] [SDK-4443] Use GitHub Actions for CI (#668) --- .github/workflows/build-and-test.yml | 27 +++++++++++++++++++++++++++ .github/workflows/dependabot.yml | 14 ++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/dependabot.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..f86ed60e --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,27 @@ +name: auth0/java-jwt/build-and-test + +on: + pull_request: + merge_group: + push: + branches: ["master", "main", "v1"] + +jobs: + gradle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c + with: + arguments: assemble apiDiff check jacocoTestReport --continue --console=plain + - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d + with: + flags: unittests + - uses: actions/upload-artifact@v3 + with: + name: Reports + path: lib/build/reports diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml new file mode 100644 index 00000000..f2839f50 --- /dev/null +++ b/.github/workflows/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "lib" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] \ No newline at end of file From 9f7eaa49384ea5f66e985ff97ef2a0520e391570 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Thu, 17 Aug 2023 23:16:04 -0500 Subject: [PATCH 267/293] Remove CircleCI (#670) --- .circleci/config.yml | 61 -------------------------------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 1675c39f..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: 2.1 -orbs: - codecov: codecov/codecov@3 - -commands: - checkout-and-build: - steps: - - checkout - - run: chmod +x gradlew - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - run: ./gradlew clean build - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} - run-tests: - steps: - - run: ./gradlew check jacocoTestReport --continue --console=plain - - codecov/upload - run-api-diff: - steps: - # run apiDiff task - - run: ./gradlew apiDiff - - store_artifacts: - path: lib/build/reports/apiDiff/apiDiff.txt - - store_artifacts: - path: lib/build/reports/apiDiff/apiDiff.html -jobs: - build: - docker: - - image: openjdk:11.0-jdk - steps: - - checkout-and-build - - run-tests - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb - api-diff: - docker: - - image: openjdk:11.0-jdk - steps: - - checkout-and-build - - run-api-diff - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb - -workflows: - build-and-test: - jobs: - - build - api-diff: - jobs: - - api-diff diff --git a/README.md b/README.md index 9f425898..4663cbdf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![A Java implementation of JSON Web Token (JWT) - RFC 7519.](https://cdn.auth0.com/website/sdks/banners/java-jwt-banner.png) -[![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) +![Build Status](https://img.shields.io/github/checks-status/auth0/java-jwt/master) [![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org/) [![Maven Central](https://img.shields.io/maven-central/v/com.auth0/java-jwt.svg?style=flat-square)](https://mvnrepository.com/artifact/com.auth0/java-jwt) From a6fa0b4d338386e7f0650b714444633420af0515 Mon Sep 17 00:00:00 2001 From: Wood Hwang Date: Wed, 13 Sep 2023 09:24:47 +0900 Subject: [PATCH 268/293] Fix typo on a comment in JWTCreator.java (#672) --- lib/src/main/java/com/auth0/jwt/JWTCreator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 0b0d21e4..bfcb9147 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -582,7 +582,7 @@ private static boolean isBasicType(Object value) { } /** - * Creates a new JWT and signs is with the given algorithm. + * Creates a new JWT and signs it with the given algorithm. * * @param algorithm used to sign the JWT * @return a new JWT token From bad6035bb87007d6744f4089857e22f8f085e1e5 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Mon, 20 Nov 2023 10:15:43 -0600 Subject: [PATCH 269/293] Remove dead README links (#676) --- lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 27d79909..248af7c5 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -132,7 +132,6 @@ public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 256 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -145,7 +144,6 @@ public static Algorithm HMAC256(String secret) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 256 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -158,7 +156,6 @@ public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 384 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -171,7 +168,6 @@ public static Algorithm HMAC384(String secret) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 384 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -184,7 +180,6 @@ public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 512 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -197,7 +192,6 @@ public static Algorithm HMAC512(String secret) throws IllegalArgumentException { * * @param secret the secret bytes to use in the verify or signing instance. * Ensure the length of the secret is at least 512 bit long - * See HMAC Key Length and Security in README * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ From d5c05d73a245b5de84830ea67565556232598fb1 Mon Sep 17 00:00:00 2001 From: Jim Anderson Date: Fri, 1 Dec 2023 06:48:30 -0600 Subject: [PATCH 270/293] empty expected audience array should throw InvalidClaimException (#679) --- lib/src/main/java/com/auth0/jwt/JWTVerifier.java | 15 +++++++++++---- .../test/java/com/auth0/jwt/JWTVerifierTest.java | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 6cec2026..bf180300 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -364,12 +364,19 @@ private boolean assertInstantIsLessThanOrEqualToNow(Instant claimVal, long leewa } private boolean assertValidAudienceClaim( - List audience, - List values, + List actualAudience, + List expectedAudience, boolean shouldContainAll ) { - return !(audience == null || (shouldContainAll && !audience.containsAll(values)) - || (!shouldContainAll && Collections.disjoint(audience, values))); + if (actualAudience == null || expectedAudience == null) { + return false; + } + + if (shouldContainAll) { + return actualAudience.containsAll(expectedAudience); + } else { + return !Collections.disjoint(actualAudience, expectedAudience); + } } private void assertPositive(long leeway) { diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index 5a784b87..732d6365 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -310,6 +310,21 @@ public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { assertThat(e.getClaimValue().asArray(String.class), is(new String[] {null})); } + @Test + public void shouldThrowWhenExpectedEmptyList() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud': 'wide audience' + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ3aWRlIGF1ZGllbmNlIn0.c9anq03XepcuEKWEVsPk9cck0sIIfrT6hHbBsCar49o"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience(new String[0]) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().asString(), is("wide audience")); + } + @Test public void shouldNotReplaceWhenMultipleChecksAreAdded() { JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) From d97f4e6df09d5437aede60776fa69e0ad49824af Mon Sep 17 00:00:00 2001 From: Kasper Karlsson Date: Tue, 6 Feb 2024 05:27:38 +0100 Subject: [PATCH 271/293] Fix typo in example code (#682) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4663cbdf..285f56ec 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ DecodedJWT decodedJWT; try { Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); JWTVerifier verifier = JWT.require(algorithm) - // specify an specific claim validations + // specify any specific claim validations .withIssuer("auth0") // reusable verifier instance .build(); From 3aa997afc11d109a8565d3101711bfb0ce110bf6 Mon Sep 17 00:00:00 2001 From: Steven Wong Date: Thu, 25 Jul 2024 22:25:13 +0800 Subject: [PATCH 272/293] Update codeowner file with new GitHub team name --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 60f116c0..7958e8bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdks-engineer +* @auth0/project-dx-sdks-engineer-codeowner From 0c76c94784e1e00010e9848b09eff2c7df1ae4d6 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 19 Dec 2023 15:07:35 +0100 Subject: [PATCH 273/293] Automate release workflow --- .github/actions/get-prerelease/action.yml | 30 +++++++ .github/actions/get-release-notes/action.yml | 42 ++++++++++ .github/actions/get-version/action.yml | 21 +++++ .github/actions/maven-publish/action.yml | 44 ++++++++++ .github/actions/release-create/action.yml | 47 +++++++++++ .github/actions/tag-exists/action.yml | 36 ++++++++ .github/workflows/java-release.yml | 88 ++++++++++++++++++++ .github/workflows/release.yml | 27 ++++++ lib/build.gradle | 16 ++-- 9 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 .github/actions/get-prerelease/action.yml create mode 100644 .github/actions/get-release-notes/action.yml create mode 100644 .github/actions/get-version/action.yml create mode 100644 .github/actions/maven-publish/action.yml create mode 100644 .github/actions/release-create/action.yml create mode 100644 .github/actions/tag-exists/action.yml create mode 100644 .github/workflows/java-release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/actions/get-prerelease/action.yml b/.github/actions/get-prerelease/action.yml new file mode 100644 index 00000000..ce7acdc3 --- /dev/null +++ b/.github/actions/get-prerelease/action.yml @@ -0,0 +1,30 @@ +name: Return a boolean indicating if the version contains prerelease identifiers + +# +# Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not. +# +# TODO: Remove once the common repo is public. +# + +inputs: + version: + required: true + +outputs: + prerelease: + value: ${{ steps.get_prerelease.outputs.PRERELEASE }} + +runs: + using: composite + + steps: + - id: get_prerelease + shell: bash + run: | + if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then + echo "PRERELEASE=true" >> $GITHUB_OUTPUT + else + echo "PRERELEASE=false" >> $GITHUB_OUTPUT + fi + env: + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-release-notes/action.yml b/.github/actions/get-release-notes/action.yml new file mode 100644 index 00000000..287d2066 --- /dev/null +++ b/.github/actions/get-release-notes/action.yml @@ -0,0 +1,42 @@ +name: Return the release notes extracted from the body of the PR associated with the release. + +# +# Returns the release notes from the content of a pull request linked to a release branch. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc. +# +# TODO: Remove once the common repo is public. +# +inputs: + version: + required: true + repo_name: + required: false + repo_owner: + required: true + token: + required: true + +outputs: + release-notes: + value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} + +runs: + using: composite + + steps: + - uses: actions/github-script@v7 + id: get_release_notes + with: + result-encoding: string + script: | + const { data: pulls } = await github.rest.pulls.list({ + owner: process.env.REPO_OWNER, + repo: process.env.REPO_NAME, + state: 'all', + head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, + }); + core.setOutput('RELEASE_NOTES', pulls[0].body); + env: + GITHUB_TOKEN: ${{ inputs.token }} + REPO_OWNER: ${{ inputs.repo_owner }} + REPO_NAME: ${{ inputs.repo_name }} + VERSION: ${{ inputs.version }} diff --git a/.github/actions/get-version/action.yml b/.github/actions/get-version/action.yml new file mode 100644 index 00000000..9440ec92 --- /dev/null +++ b/.github/actions/get-version/action.yml @@ -0,0 +1,21 @@ +name: Return the version extracted from the branch name + +# +# Returns the version from the .version file. +# +# TODO: Remove once the common repo is public. +# + +outputs: + version: + value: ${{ steps.get_version.outputs.VERSION }} + +runs: + using: composite + + steps: + - id: get_version + shell: bash + run: | + VERSION=$(head -1 .version) + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml new file mode 100644 index 00000000..ee477061 --- /dev/null +++ b/.github/actions/maven-publish/action.yml @@ -0,0 +1,44 @@ +name: Publish release to Java + +inputs: + ossr-username: + required: true + ossr-password: + required: true + signing-key: + required: true + signing-password: + required: true + java-version: + required: true + is-android: + required: true + version: + required: true + +runs: + using: composite + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Java + shell: bash + run: | + curl -s "https://get.sdkman.io" | bash + source "/home/runner/.sdkman/bin/sdkman-init.sh" + sdk list java + sdk install java ${{ inputs.java-version }} && sdk default java ${{ inputs.java-version }} + + - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 + + - name: Publish Java + shell: bash + if: inputs.is-android == 'false' + run: ./gradlew clean assemble sign publishMavenJavaPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" + + - name: Publish Android + shell: bash + if: inputs.is-android == 'true' + run: ./gradlew clean assemble sign publishAndroidLibraryPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" diff --git a/.github/actions/release-create/action.yml b/.github/actions/release-create/action.yml new file mode 100644 index 00000000..6a2bf804 --- /dev/null +++ b/.github/actions/release-create/action.yml @@ -0,0 +1,47 @@ +name: Create a GitHub release + +# +# Creates a GitHub release with the given version. +# +# TODO: Remove once the common repo is public. +# + +inputs: + token: + required: true + files: + required: false + name: + required: true + body: + required: true + tag: + required: true + commit: + required: true + draft: + default: false + required: false + prerelease: + default: false + required: false + fail_on_unmatched_files: + default: true + required: false + +runs: + using: composite + + steps: + - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 + with: + body: ${{ inputs.body }} + name: ${{ inputs.name }} + tag_name: ${{ inputs.tag }} + target_commitish: ${{ inputs.commit }} + draft: ${{ inputs.draft }} + prerelease: ${{ inputs.prerelease }} + fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} + files: ${{ inputs.files }} + env: + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/actions/tag-exists/action.yml b/.github/actions/tag-exists/action.yml new file mode 100644 index 00000000..b5fbdb73 --- /dev/null +++ b/.github/actions/tag-exists/action.yml @@ -0,0 +1,36 @@ +name: Return a boolean indicating if a tag already exists for the repository + +# +# Returns a simple true/false boolean indicating whether the tag exists or not. +# +# TODO: Remove once the common repo is public. +# + +inputs: + token: + required: true + tag: + required: true + +outputs: + exists: + description: 'Whether the tag exists or not' + value: ${{ steps.tag-exists.outputs.EXISTS }} + +runs: + using: composite + + steps: + - id: tag-exists + shell: bash + run: | + GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" + http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") + if [ "$http_status_code" -ne "404" ] ; then + echo "EXISTS=true" >> $GITHUB_OUTPUT + else + echo "EXISTS=false" >> $GITHUB_OUTPUT + fi + env: + TAG_NAME: ${{ inputs.tag }} + GITHUB_TOKEN: ${{ inputs.token }} diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 00000000..3f81eb14 --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,88 @@ +name: Create Java and GitHub Release + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + is-android: + required: true + type: string + secrets: + ossr-username: + required: true + ossr-password: + required: true + signing-key: + required: true + signing-password: + required: true + github-token: + required: true + +### TODO: Replace instances of './.github/actions/' w/ `auth0/dx-sdk-actions/` and append `@latest` after the common `dx-sdk-actions` repo is made public. +### TODO: Also remove `get-prerelease`, `get-version`, `release-create`, `tag-create` and `tag-exists` actions from this repo's .github/actions folder once the repo is public. + +jobs: + release: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + environment: release + + steps: + # Checkout the code + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Get the version from the branch name + - id: get_version + uses: ./.github/actions/get-version + + # Get the prerelease flag from the branch name + - id: get_prerelease + uses: ./.github/actions/get-prerelease + with: + version: ${{ steps.get_version.outputs.version }} + + # Get the release notes + - id: get_release_notes + uses: ./.github/actions/get-release-notes + with: + token: ${{ secrets.github-token }} + version: ${{ steps.get_version.outputs.version }} + repo_owner: ${{ github.repository_owner }} + repo_name: ${{ github.event.repository.name }} + + # Check if the tag already exists + - id: tag_exists + uses: ./.github/actions/tag-exists + with: + tag: ${{ steps.get_version.outputs.version }} + token: ${{ secrets.github-token }} + + # If the tag already exists, exit with an error + - if: steps.tag_exists.outputs.exists == 'true' + run: exit 1 + + # Publish the release to Maven + - uses: ./.github/actions/maven-publish + with: + java-version: ${{ inputs.java-version }} + is-android: ${{ inputs.is-android }} + version: ${{ steps.get_version.outputs.version }} + ossr-username: ${{ secrets.ossr-username }} + ossr-password: ${{ secrets.ossr-password }} + signing-key: ${{ secrets.signing-key }} + signing-password: ${{ secrets.signing-password }} + + # Create a release for the tag + - uses: ./.github/actions/release-create + with: + token: ${{ secrets.github-token }} + name: ${{ steps.get_version.outputs.version }} + body: ${{ steps.get_release_notes.outputs.release-notes }} + tag: ${{ steps.get_version.outputs.version }} + commit: ${{ github.sha }} + prerelease: ${{ steps.get_prerelease.outputs.prerelease }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..63482cca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Create GitHub Release + +on: + pull_request: + types: + - closed + workflow_dispatch: + +permissions: + contents: write + +### TODO: Replace instances of './.github/workflows/' w/ `auth0/dx-sdk-actions/workflows/` and append `@latest` after the common `dx-sdk-actions` repo is made public. +### TODO: Also remove `get-prerelease`, `get-release-notes`, `get-version`, `maven-publish`, `release-create`, and `tag-exists` actions from this repo's .github/actions folder once the repo is public. +### TODO: Also remove `java-release` workflow from this repo's .github/workflows folder once the repo is public. + +jobs: + release: + uses: ./.github/workflows/java-release.yml + with: + java-version: 8.0.382-tem + is-android: false + secrets: + ossr-username: ${{ secrets.OSSR_USERNAME }} + ossr-password: ${{ secrets.OSSR_PASSWORD }} + signing-key: ${{ secrets.SIGNING_KEY }} + signing-password: ${{ secrets.SIGNING_PASSWORD }} + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/lib/build.gradle b/lib/build.gradle index c4e11764..aa134c32 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -31,12 +31,16 @@ tasks.named("checkstyleJmh").configure({ logger.lifecycle("Using version ${version} for ${group}.${name}") +def signingKey = findProperty('signingKey') +def signingKeyPwd = findProperty('signingPassword') + oss { name "java jwt" repository "java-jwt" organization "auth0" description "Java implementation of JSON Web Token (JWT)" baselineCompareVersion "4.1.0" + skipAssertSigningConfiguration true developers { auth0 { @@ -54,6 +58,10 @@ oss { } } +signing { + useInMemoryPgpKeys(signingKey, signingKeyPwd) +} + java { toolchain { languageVersion = JavaLanguageVersion.of(11) @@ -158,14 +166,6 @@ jar { compileModuleInfoJava.dependsOn compileJava classes.dependsOn compileModuleInfoJava -// Creates a version.txt file containing the current version of the SDK. -// This file is picked up and parsed by our Ship Orb to determine the version. -task exportVersion() { - doLast { - new File(rootDir, "version.txt").text = "$version" - } -} - // you can pass any arguments JMH accepts via Gradle args. // Example: ./gradlew runJMH --args="-lrf" tasks.register('runJMH', JavaExec) { From 05fef18c2b3cdfd5ece2cb51ebcbff5a6ebce127 Mon Sep 17 00:00:00 2001 From: Poovamraj T T Date: Tue, 19 Dec 2023 15:10:35 +0100 Subject: [PATCH 274/293] Add .version and modify .shiprc --- .shiprc | 1 + .version | 1 + 2 files changed, 2 insertions(+) create mode 100644 .version diff --git a/.shiprc b/.shiprc index fe59345e..1b83cc62 100644 --- a/.shiprc +++ b/.shiprc @@ -1,6 +1,7 @@ { "files": { "README.md": [], + ".version": [], "lib/build.gradle": ["version = \"{MAJOR}.{MINOR}.{PATCH}\""] }, "prefixVersion": false diff --git a/.version b/.version new file mode 100644 index 00000000..64b5ae39 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +4.4.0 \ No newline at end of file From 9cd2b043280e3f66778d75198aa34bf821696c3c Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Wed, 20 Dec 2023 14:26:46 +0100 Subject: [PATCH 275/293] Update .github/workflows/release.yml Co-authored-by: Jim Anderson --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63482cca..7a98f05e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: release: uses: ./.github/workflows/java-release.yml with: - java-version: 8.0.382-tem + java-version: 11.0.21-tem is-android: false secrets: ossr-username: ${{ secrets.OSSR_USERNAME }} From 876d7456d362e031fcb1a56609e2c850bede426f Mon Sep 17 00:00:00 2001 From: Tanya Sinha Date: Thu, 14 Nov 2024 16:05:59 +0530 Subject: [PATCH 276/293] Add reversing lab scanner (#695) --- .github/actions/rl-scanner/action.yml | 66 ++++++++++++++++++++++++ .github/workflows/release.yml | 14 +++++ .github/workflows/rl-secure.yml | 73 +++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 .github/actions/rl-scanner/action.yml create mode 100644 .github/workflows/rl-secure.yml diff --git a/.github/actions/rl-scanner/action.yml b/.github/actions/rl-scanner/action.yml new file mode 100644 index 00000000..fbf81217 --- /dev/null +++ b/.github/actions/rl-scanner/action.yml @@ -0,0 +1,66 @@ +name: 'Reversing Labs Scanner' +description: 'Runs the Reversing Labs scanner on a specified artifact.' +inputs: + artifact-path: + description: 'Path to the artifact to be scanned.' + required: true + version: + description: 'Version of the artifact.' + required: true + +runs: + using: 'composite' + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install Python dependencies + shell: bash + run: | + pip install boto3 requests + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} + aws-region: us-east-1 + mask-aws-account-id: true + + - name: Install RL Wrapper + shell: bash + run: | + pip install rl-wrapper>=1.0.0 --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python-local/simple" + - name: Run RL Scanner + shell: bash + env: + RLSECURE_LICENSE: ${{ env.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ env.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ env.SIGNAL_HANDLER_TOKEN }} + PYTHONUNBUFFERED: 1 + run: | + if [ ! -f "${{ inputs.artifact-path }}" ]; then + echo "Artifact not found: ${{ inputs.artifact-path }}" + exit 1 + fi + rl-wrapper \ + --artifact "${{ inputs.artifact-path }}" \ + --name "${{ github.event.repository.name }}" \ + --version "${{ inputs.version }}" \ + --repository "${{ github.repository }}" \ + --commit "${{ github.sha }}" \ + --build-env "github_actions" \ + --suppress_output + # Check the outcome of the scanner + if [ $? -ne 0 ]; then + echo "RL Scanner failed." + echo "scan-status=failed" >> $GITHUB_ENV + exit 1 + else + echo "RL Scanner passed." + echo "scan-status=success" >> $GITHUB_ENV + fi +outputs: + scan-status: + description: 'The outcome of the scan process.' + value: ${{ env.scan-status }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a98f05e..49e48059 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,14 +8,28 @@ on: permissions: contents: write + id-token: write # This is required for requesting the JWT ### TODO: Replace instances of './.github/workflows/' w/ `auth0/dx-sdk-actions/workflows/` and append `@latest` after the common `dx-sdk-actions` repo is made public. ### TODO: Also remove `get-prerelease`, `get-release-notes`, `get-version`, `maven-publish`, `release-create`, and `tag-exists` actions from this repo's .github/actions folder once the repo is public. ### TODO: Also remove `java-release` workflow from this repo's .github/workflows folder once the repo is public. jobs: + rl-scanner: + uses: ./.github/workflows/rl-secure.yml + with: + java-version: 11 + artifact-name: 'java-jwt.tgz' + secrets: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} release: uses: ./.github/workflows/java-release.yml + needs: rl-scanner with: java-version: 11.0.21-tem is-android: false diff --git a/.github/workflows/rl-secure.yml b/.github/workflows/rl-secure.yml new file mode 100644 index 00000000..ef329594 --- /dev/null +++ b/.github/workflows/rl-secure.yml @@ -0,0 +1,73 @@ +name: RL-Secure Workflow + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + artifact-name: + required: true + type: string + secrets: + RLSECURE_LICENSE: + required: true + RLSECURE_SITE_KEY: + required: true + SIGNAL_HANDLER_TOKEN: + required: true + PRODSEC_TOOLS_USER: + required: true + PRODSEC_TOOLS_TOKEN: + required: true + PRODSEC_TOOLS_ARN: + required: true + +jobs: + checkout-build-scan-only: + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) + runs-on: ubuntu-latest + outputs: + scan-status: ${{ steps.rl-scan-conclusion.outcome }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ inputs.java-version }} + + - name: Build with Gradle + uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c + with: + arguments: assemble apiDiff check jacocoTestReport --continue --console=plain + + - name: Get Artifact Version + id: get_version + uses: ./.github/actions/get-version + + - name: Create tgz build artifact + run: | + tar -czvf ${{ inputs.artifact-name }} * + + - name: Run RL Scanner + id: rl-scan-conclusion + uses: ./.github/actions/rl-scanner + with: + artifact-path: "$(pwd)/${{ inputs.artifact-name }}" + version: "${{ steps.get_version.outputs.version }}" + env: + RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} + RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} + SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} + PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} + PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} + PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} + + - name: Output scan result + run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV \ No newline at end of file From 41d93afce36dd07431f45f39401f24840835f54e Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 15 Nov 2024 21:12:00 +0530 Subject: [PATCH 277/293] added snyk workflow --- .github/workflows/snyk.yml | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/snyk.yml diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 00000000..9394832d --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,47 @@ +name: Snyk + +on: + merge_group: + workflow_dispatch: + pull_request_target: + types: + - opened + - synchronize + push: + branches: + - master + schedule: + - cron: '30 0 1,15 * *' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +jobs: + authorize: + name: Authorize + environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }} + runs-on: ubuntu-latest + steps: + - run: true + + check: + needs: authorize + + name: Check for Vulnerabilities + runs-on: ubuntu-latest + + steps: + - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' + run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. + + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - uses: snyk/actions/gradle-jdk11@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0 + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} \ No newline at end of file From b915cb6ebe089ac68ae70221792576d322c9a924 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 15 Nov 2024 21:16:11 +0530 Subject: [PATCH 278/293] removed pull_request_target --- .github/workflows/snyk.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 9394832d..457b6afa 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -3,7 +3,7 @@ name: Snyk on: merge_group: workflow_dispatch: - pull_request_target: + pull_request: types: - opened - synchronize @@ -21,16 +21,8 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} jobs: - authorize: - name: Authorize - environment: ${{ github.actor != 'dependabot[bot]' && github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && 'external' || 'internal' }} - runs-on: ubuntu-latest - steps: - - run: true check: - needs: authorize - name: Check for Vulnerabilities runs-on: ubuntu-latest From de23d8f21f49e4d7a5b5572ac1416d13f2ddc5e8 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 3 Jan 2025 18:21:41 +0530 Subject: [PATCH 279/293] add java 21 in CI --- .github/workflows/build-and-test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f86ed60e..32886d68 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,12 +9,17 @@ on: jobs: gradle: runs-on: ubuntu-latest + + strategy: + matrix: + java-version: [11, 17, 21] + steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: distribution: temurin - java-version: 11 + java-version: ${{ matrix.java-version }} - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c with: arguments: assemble apiDiff check jacocoTestReport --continue --console=plain From 9e04f118db4f3356e673aa981be95a56f8ddf84d Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 3 Jan 2025 18:25:53 +0530 Subject: [PATCH 280/293] added java 21 --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 32886d68..5ca24522 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - java-version: [11, 17, 21] + java-version: [11, 21] steps: - uses: actions/checkout@v3 From cc705395f5099d09a8a33a0ea35c87915e38aa66 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 17 Jan 2025 10:27:38 +0530 Subject: [PATCH 281/293] added java 21 tests --- .github/workflows/build-and-test.yml | 7 +------ lib/build.gradle | 11 +++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5ca24522..f86ed60e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,17 +9,12 @@ on: jobs: gradle: runs-on: ubuntu-latest - - strategy: - matrix: - java-version: [11, 21] - steps: - uses: actions/checkout@v3 - uses: actions/setup-java@v3 with: distribution: temurin - java-version: ${{ matrix.java-version }} + java-version: 11 - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c with: arguments: assemble apiDiff check jacocoTestReport --continue --console=plain diff --git a/lib/build.gradle b/lib/build.gradle index aa134c32..77de85a7 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -154,9 +154,20 @@ def testJava17 = tasks.register('testJava17', Test) { shouldRunAfter(tasks.named('test')) } +def testJava21 = tasks.register('testJava21', Test) { + description = 'Runs unit tests on Java 21.' + group = 'verification' + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + }) + shouldRunAfter(tasks.named('test')) +} + tasks.named('check') { dependsOn(testJava8) dependsOn(testJava17) + dependsOn(testJava21) } jar { From d20aec6d58cd37ab518b987cd811bb6480a2ff97 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 17 Jan 2025 10:35:41 +0530 Subject: [PATCH 282/293] updated jacoco toll version --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index 77de85a7..bff7bb9c 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -94,7 +94,7 @@ dependencies { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.10" } jacocoTestReport { From 8278f62be812496210c5fa5a11192741d2d6c8c7 Mon Sep 17 00:00:00 2001 From: Carlos Galan Cladera Date: Wed, 11 Dec 2024 14:53:41 +0100 Subject: [PATCH 283/293] fix: upgrade jackson-core to 2.15 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index aa134c32..c2c77533 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -80,7 +80,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' From 3840a1eaf07e2de55c7676b90d6b398526355ff1 Mon Sep 17 00:00:00 2001 From: Carlos Galan Cladera Date: Wed, 11 Dec 2024 15:11:01 +0100 Subject: [PATCH 284/293] fix: upgrade jackson-core to 2.15.4 --- lib/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle b/lib/build.gradle index c2c77533..16a55af1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -80,7 +80,7 @@ javadoc { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.4' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' testImplementation 'junit:junit:4.13.2' From a52bec0224ea087116c807d8f895fa98d1596013 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 22 Jan 2025 14:02:26 +0530 Subject: [PATCH 285/293] added maven-publish changes --- .github/actions/maven-publish/action.yml | 25 ++--- .github/workflows/java-release.yml | 9 +- .github/workflows/release.yml | 3 +- gradle.properties | 22 +++++ gradle/maven-publish.gradle | 113 +++++++++++++++++++++++ gradle/versioning.gradle | 17 ++++ lib/build.gradle | 89 +++++++++++++----- settings.gradle | 3 - 8 files changed, 231 insertions(+), 50 deletions(-) create mode 100644 gradle/maven-publish.gradle create mode 100644 gradle/versioning.gradle diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml index ee477061..0d280cbe 100644 --- a/.github/actions/maven-publish/action.yml +++ b/.github/actions/maven-publish/action.yml @@ -1,20 +1,16 @@ name: Publish release to Java inputs: + java-version: + required: true ossr-username: required: true - ossr-password: + ossr-token: required: true signing-key: required: true signing-password: required: true - java-version: - required: true - is-android: - required: true - version: - required: true runs: using: composite @@ -33,12 +29,11 @@ runs: - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 - - name: Publish Java - shell: bash - if: inputs.is-android == 'false' - run: ./gradlew clean assemble sign publishMavenJavaPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" - - - name: Publish Android + - name: Publish Android/Java Packages to Maven shell: bash - if: inputs.is-android == 'true' - run: ./gradlew clean assemble sign publishAndroidLibraryPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" + run: ./gradlew publish -PisSnapshot=false --stacktrace + env: + MAVEN_USERNAME: ${{ inputs.ossr-username }} + MAVEN_PASSWORD: ${{ inputs.ossr-token }} + SIGNING_KEY: ${{ inputs.signing-key}} + SIGNING_PASSWORD: ${{ inputs.signing-password}} \ No newline at end of file diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index 3f81eb14..ddce3e59 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -6,13 +6,10 @@ on: java-version: required: true type: string - is-android: - required: true - type: string secrets: ossr-username: required: true - ossr-password: + ossr-token: required: true signing-key: required: true @@ -70,10 +67,8 @@ jobs: - uses: ./.github/actions/maven-publish with: java-version: ${{ inputs.java-version }} - is-android: ${{ inputs.is-android }} - version: ${{ steps.get_version.outputs.version }} ossr-username: ${{ secrets.ossr-username }} - ossr-password: ${{ secrets.ossr-password }} + ossr-token: ${{ secrets.ossr-token }} signing-key: ${{ secrets.signing-key }} signing-password: ${{ secrets.signing-password }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49e48059..2b00e426 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,9 @@ jobs: needs: rl-scanner with: java-version: 11.0.21-tem - is-android: false secrets: ossr-username: ${{ secrets.OSSR_USERNAME }} - ossr-password: ${{ secrets.OSSR_PASSWORD }} + ossr-token: ${{ secrets.OSSR_TOKEN }} signing-key: ${{ secrets.SIGNING_KEY }} signing-password: ${{ secrets.SIGNING_PASSWORD }} github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/gradle.properties b/gradle.properties index aac7c9b4..b4d8583f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,25 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + +GROUP=com.auth0 +POM_ARTIFACT_ID=java-jwt +VERSION_NAME=4.4.0 + +POM_NAME=java jwt +POM_DESCRIPTION=Java client library for the Auth0 platform +POM_PACKAGING=jar + +POM_URL=https://github.com/auth0/java-jwt +POM_SCM_URL=https://github.com/auth0/java-jwt + +POM_SCM_CONNECTION=scm:git:https://github.com/auth0/java-jwt.git +POM_SCM_DEV_CONNECTION=scm:git:https://github.com/auth0/java-jwt.git + +POM_LICENCE_NAME=The MIT License (MIT) +POM_LICENCE_URL=https://raw.githubusercontent.com/auth0/java-jwt/master/LICENSE +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=auth0 +POM_DEVELOPER_NAME=Auth0 +POM_DEVELOPER_EMAIL=oss@auth0.com \ No newline at end of file diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle new file mode 100644 index 00000000..206a581b --- /dev/null +++ b/gradle/maven-publish.gradle @@ -0,0 +1,113 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +task('sourcesJar', type: Jar, dependsOn: classes) { + archiveClassifier = 'sources' + from sourceSets.main.allSource +} + +task('javadocJar', type: Jar, dependsOn: javadoc) { + archiveClassifier = 'javadoc' + from javadoc.getDestinationDir() +} +tasks.withType(Javadoc).configureEach { + javadocTool = javaToolchains.javadocToolFor { + // Use latest JDK for javadoc generation + languageVersion = JavaLanguageVersion.of(17) + } +} + +javadoc { + // Specify the Java version that the project will use + options.addStringOption('-release', "8") +} +artifacts { + archives sourcesJar, javadocJar +} + + +final releaseRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +final snapshotRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + +publishing { + publications { + mavenJava(MavenPublication) { + + groupId = GROUP + artifactId = POM_ARTIFACT_ID + version = getVersionName() + + artifact("$buildDir/libs/${project.name}-${version}.jar") + artifact sourcesJar + artifact javadocJar + + pom { + name = POM_NAME + packaging = POM_PACKAGING + description = POM_DESCRIPTION + url = POM_URL + + licenses { + license { + name = POM_LICENCE_NAME + url = POM_LICENCE_URL + distribution = POM_LICENCE_DIST + } + } + + developers { + developer { + id = POM_DEVELOPER_ID + name = POM_DEVELOPER_NAME + email = POM_DEVELOPER_EMAIL + } + } + + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + + pom.withXml { + def dependenciesNode = asNode().appendNode('dependencies') + + project.configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } + repositories { + maven { + name = "sonatype" + url = version.endsWith('SNAPSHOT') ? snapshotRepositoryUrl : releaseRepositoryUrl + credentials { + username = System.getenv("MAVEN_USERNAME") + password = System.getenv("MAVEN_PASSWORD") + } + } + } +} + +signing { + def signingKey = System.getenv("SIGNING_KEY") + def signingPassword = System.getenv("SIGNING_PASSWORD") + useInMemoryPgpKeys(signingKey, signingPassword) + + sign publishing.publications.mavenJava +} + +javadoc { + if(JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } +} + +tasks.named('publish').configure { + dependsOn tasks.named('assemble') +} \ No newline at end of file diff --git a/gradle/versioning.gradle b/gradle/versioning.gradle new file mode 100644 index 00000000..3441ae11 --- /dev/null +++ b/gradle/versioning.gradle @@ -0,0 +1,17 @@ +def getVersionFromFile() { + def versionFile = rootProject.file('.version') + return versionFile.text.readLines().first().trim() +} + +def isSnapshot() { + return hasProperty('isSnapshot') ? isSnapshot.toBoolean() : true +} + +def getVersionName() { + return isSnapshot() ? project.version+"-SNAPSHOT" : project.version +} + +ext { + getVersionName = this.&getVersionName + getVersionFromFile = this.&getVersionFromFile +} \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 148bce72..167f234a 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,9 +1,19 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + // https://github.com/melix/japicmp-gradle-plugin/issues/36 + classpath 'com.google.guava:guava:31.1-jre' + } +} plugins { id 'java' id 'jacoco' - id 'com.auth0.gradle.oss-library.java' id 'checkstyle' + id 'me.champeau.gradle.japicmp' version '0.2.9' } sourceSets { @@ -29,37 +39,69 @@ tasks.named("checkstyleJmh").configure({ enabled = false }) -logger.lifecycle("Using version ${version} for ${group}.${name}") +apply from: rootProject.file('gradle/versioning.gradle') + +version = getVersionFromFile() +group = GROUP +logger.lifecycle("Using version ${version} for ${name} group $group") -def signingKey = findProperty('signingKey') -def signingKeyPwd = findProperty('signingPassword') +import me.champeau.gradle.japicmp.JapicmpTask -oss { - name "java jwt" - repository "java-jwt" - organization "auth0" - description "Java implementation of JSON Web Token (JWT)" - baselineCompareVersion "4.1.0" - skipAssertSigningConfiguration true +project.afterEvaluate { - developers { - auth0 { - displayName = "Auth0" - email = "oss@auth0.com" + def versions = project.ext.testInJavaVersions + for (pluginJavaTestVersion in versions) { + def taskName = "testInJava-${pluginJavaTestVersion}" + tasks.register(taskName, Test) { + def versionToUse = taskName.split("-").getAt(1) as Integer + description = "Runs unit tests on Java version ${versionToUse}." + project.logger.quiet("Test will be running in ${versionToUse}") + group = 'verification' + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(versionToUse) + }) + shouldRunAfter(tasks.named('test')) } - lbalmaceda { - displayName = "Luciano Balmaceda" - email = "luciano.balmaceda@auth0.com" + tasks.named('check') { + dependsOn(taskName) } - hzalaz { - displayName = "Hernan Zalazar" - email = "hernan@auth0.com" + } + + project.configure(project) { + def baselineVersion = project.ext.baselineCompareVersion + task('apiDiff', type: JapicmpTask, dependsOn: 'jar') { + oldClasspath = files(getBaselineJar(project, baselineVersion)) + newClasspath = files(jar.archiveFile) + onlyModified = true + failOnModification = true + ignoreMissingClasses = true + htmlOutputFile = file("$buildDir/reports/apiDiff/apiDiff.html") + txtOutputFile = file("$buildDir/reports/apiDiff/apiDiff.txt") + doLast { + project.logger.quiet("Comparing against baseline version ${baselineVersion}") + } + } + } +} + +private static File getBaselineJar(Project project, String baselineVersion) { + // Use detached configuration: https://github.com/square/okhttp/blob/master/build.gradle#L270 + def group = project.group + try { + def baseline = "${project.group}:${project.name}:$baselineVersion" + project.group = 'virtual_group_for_japicmp' + def dependency = project.dependencies.create(baseline + "@jar") + return project.configurations.detachedConfiguration(dependency).files.find { + it.name == "${project.name}-${baselineVersion}.jar" } + } finally { + project.group = group } } -signing { - useInMemoryPgpKeys(signingKey, signingKeyPwd) +ext { + baselineCompareVersion = '4.1.0' + testInJavaVersions = [8, 11, 17, 21] } java { @@ -198,3 +240,4 @@ tasks.register('jmhHelp', JavaExec) { args '-h' } +apply from: rootProject.file('gradle/maven-publish.gradle') diff --git a/settings.gradle b/settings.gradle index 8d5f112c..d3c4c85b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,9 +2,6 @@ pluginManagement { repositories { gradlePluginPortal() } - plugins { - id 'com.auth0.gradle.oss-library.java' version '0.17.2' - } } include ':java-jwt' From 2643ca2a464dbb159377b67ae32c9e0b7c276b55 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 22 Jan 2025 15:55:30 +0530 Subject: [PATCH 286/293] removed version from gralde properties --- gradle.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b4d8583f..74a5a049 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,6 @@ org.gradle.jvmargs=-Xmx1536m GROUP=com.auth0 POM_ARTIFACT_ID=java-jwt -VERSION_NAME=4.4.0 POM_NAME=java jwt POM_DESCRIPTION=Java client library for the Auth0 platform From 87710ed5aca11004ee7bc104fb60bb7c5994b0a5 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 22 Jan 2025 19:12:50 +0530 Subject: [PATCH 287/293] updated version --- gradle/maven-publish.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/maven-publish.gradle b/gradle/maven-publish.gradle index 206a581b..a9ad38d3 100644 --- a/gradle/maven-publish.gradle +++ b/gradle/maven-publish.gradle @@ -19,7 +19,7 @@ tasks.withType(Javadoc).configureEach { javadoc { // Specify the Java version that the project will use - options.addStringOption('-release', "8") + options.addStringOption('-release', "11") } artifacts { archives sourcesJar, javadocJar From 65181f8838bcd8cebb8585c8024baba9680d77d9 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 22 Jan 2025 19:47:05 +0530 Subject: [PATCH 288/293] Release 4.5.0 --- .version | 2 +- CHANGELOG.md | 14 ++++++++++++++ README.md | 4 ++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.version b/.version index 64b5ae39..ae153944 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -4.4.0 \ No newline at end of file +4.5.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1e75bd..6bb634fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## [4.5.0](https://github.com/auth0/java-jwt/tree/4.5.0) (2025-01-22) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.4.0...4.5.0) + +**Added** +- Fix jackson vuln [\#705](https://github.com/auth0/java-jwt/pull/705) ([tanya732](https://github.com/tanya732)) +- Fix typo in example code [\#682](https://github.com/auth0/java-jwt/pull/682) ([kasperkarlsson](https://github.com/kasperkarlsson)) +- Remove dead README links [\#676](https://github.com/auth0/java-jwt/pull/676) ([jimmyjames](https://github.com/jimmyjames)) +- Fix typo on a comment in JWTCreator.java [\#672](https://github.com/auth0/java-jwt/pull/672) ([sgc109](https://github.com/sgc109)) +- Remove CircleCI [\#670](https://github.com/auth0/java-jwt/pull/670) ([jimmyjames](https://github.com/jimmyjames)) +- Empty string audience claim should be deserialized as empty string [\#663](https://github.com/auth0/java-jwt/pull/663) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- empty expected audience array should throw InvalidClaimException [\#679](https://github.com/auth0/java-jwt/pull/679) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.4.0](https://github.com/auth0/java-jwt/tree/4.4.0) (2023-03-31) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.3.0...4.4.0) diff --git a/README.md b/README.md index 285f56ec..9d0ae41c 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ Add the dependency via Maven: com.auth0 java-jwt - 4.4.0 + 4.5.0 ``` or Gradle: ```gradle -implementation 'com.auth0:java-jwt:4.4.0' +implementation 'com.auth0:java-jwt:4.5.0' ``` ### Create a JWT From 6e76728a92249fd8595e79ee8fdd742a063aa61c Mon Sep 17 00:00:00 2001 From: tanya732 Date: Mon, 27 Jan 2025 12:24:01 +0530 Subject: [PATCH 289/293] upgraded plugin --- lib/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle b/lib/build.gradle index 167f234a..83093fc1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -13,7 +13,7 @@ plugins { id 'java' id 'jacoco' id 'checkstyle' - id 'me.champeau.gradle.japicmp' version '0.2.9' + id 'me.champeau.gradle.japicmp' version '0.4.1' } sourceSets { @@ -70,8 +70,8 @@ project.afterEvaluate { project.configure(project) { def baselineVersion = project.ext.baselineCompareVersion task('apiDiff', type: JapicmpTask, dependsOn: 'jar') { - oldClasspath = files(getBaselineJar(project, baselineVersion)) - newClasspath = files(jar.archiveFile) + oldClasspath.from(files(getBaselineJar(project, baselineVersion))) + newClasspath.from(files(jar.archiveFile)) onlyModified = true failOnModification = true ignoreMissingClasses = true @@ -122,6 +122,7 @@ javadoc { } dependencies { + implementation 'com.fasterxml.jackson.core:jackson-core:2.15.4' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.4' testImplementation 'org.bouncycastle:bcprov-jdk15on:1.70' From dcbc560d44903fb47012b291beebad60fe37da73 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Tue, 28 Jan 2025 10:58:19 +0530 Subject: [PATCH 290/293] added JAVA_HOME path --- .github/actions/maven-publish/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml index 0d280cbe..88fdaa1a 100644 --- a/.github/actions/maven-publish/action.yml +++ b/.github/actions/maven-publish/action.yml @@ -26,6 +26,8 @@ runs: source "/home/runner/.sdkman/bin/sdkman-init.sh" sdk list java sdk install java ${{ inputs.java-version }} && sdk default java ${{ inputs.java-version }} + export JAVA_HOME=${SDKMAN_DIR}/candidates/java/current + echo "JAVA_HOME is set to $JAVA_HOME" - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 From ca4f842509e7523b9a6e0f94b1f9b02e8509be93 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Tue, 28 Jan 2025 11:16:45 +0530 Subject: [PATCH 291/293] Release 4.5.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb634fb..577ccf1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Change Log +## [4.5.0](https://github.com/auth0/java-jwt/tree/4.5.0) (2025-01-28) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.4.0...4.5.0) + +**Added** +- Upgraded Plugin [\#711](https://github.com/auth0/java-jwt/pull/711) ([tanya732](https://github.com/tanya732)) +- Fix jackson vuln [\#705](https://github.com/auth0/java-jwt/pull/705) ([tanya732](https://github.com/tanya732)) +- Fix typo in example code [\#682](https://github.com/auth0/java-jwt/pull/682) ([kasperkarlsson](https://github.com/kasperkarlsson)) +- Remove dead README links [\#676](https://github.com/auth0/java-jwt/pull/676) ([jimmyjames](https://github.com/jimmyjames)) +- Fix typo on a comment in JWTCreator.java [\#672](https://github.com/auth0/java-jwt/pull/672) ([sgc109](https://github.com/sgc109)) +- Remove CircleCI [\#670](https://github.com/auth0/java-jwt/pull/670) ([jimmyjames](https://github.com/jimmyjames)) +- Empty string audience claim should be deserialized as empty string [\#663](https://github.com/auth0/java-jwt/pull/663) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- empty expected audience array should throw InvalidClaimException [\#679](https://github.com/auth0/java-jwt/pull/679) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.5.0](https://github.com/auth0/java-jwt/tree/4.5.0) (2025-01-22) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.4.0...4.5.0) From 05bc0027837cf85be597efbea51ce7b2104136d9 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 29 Jan 2025 03:09:12 +0530 Subject: [PATCH 292/293] added JAVA_HOME --- .github/actions/maven-publish/action.yml | 3 +++ .github/workflows/java-release.yml | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/.github/actions/maven-publish/action.yml b/.github/actions/maven-publish/action.yml index 88fdaa1a..01e3a621 100644 --- a/.github/actions/maven-publish/action.yml +++ b/.github/actions/maven-publish/action.yml @@ -30,11 +30,14 @@ runs: echo "JAVA_HOME is set to $JAVA_HOME" - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 + env: + JAVA_HOME: ${{ env.JAVA_HOME }} - name: Publish Android/Java Packages to Maven shell: bash run: ./gradlew publish -PisSnapshot=false --stacktrace env: + JAVA_HOME: ${{ env.JAVA_HOME }} MAVEN_USERNAME: ${{ inputs.ossr-username }} MAVEN_PASSWORD: ${{ inputs.ossr-token }} SIGNING_KEY: ${{ inputs.signing-key}} diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml index ddce3e59..00771307 100644 --- a/.github/workflows/java-release.yml +++ b/.github/workflows/java-release.yml @@ -63,6 +63,12 @@ jobs: - if: steps.tag_exists.outputs.exists == 'true' run: exit 1 + # Set JAVA_HOME here and pass it to subsequent steps + - name: Set JAVA_HOME for Gradle + run: echo "JAVA_HOME=/home/runner/.sdkman/candidates/java/current" >> $GITHUB_ENV # This ensures JAVA_HOME is set globally + env: + SDKMAN_DIR: /home/runner/.sdkman + # Publish the release to Maven - uses: ./.github/actions/maven-publish with: @@ -71,6 +77,8 @@ jobs: ossr-token: ${{ secrets.ossr-token }} signing-key: ${{ secrets.signing-key }} signing-password: ${{ secrets.signing-password }} + env: + JAVA_HOME: ${{ env.JAVA_HOME }} # Create a release for the tag - uses: ./.github/actions/release-create From 051e1c3efba283c5dc6812f7bffb715995d4794c Mon Sep 17 00:00:00 2001 From: tanya732 Date: Wed, 29 Jan 2025 10:21:20 +0530 Subject: [PATCH 293/293] Release 4.5.0 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 577ccf1b..b97fab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Change Log +## [4.5.0](https://github.com/auth0/java-jwt/tree/4.5.0) (2025-01-29) +[Full Changelog](https://github.com/auth0/java-jwt/compare/4.4.0...4.5.0) + +**Added** +- Upgraded Plugin [\#711](https://github.com/auth0/java-jwt/pull/711) ([tanya732](https://github.com/tanya732)) +- Fix jackson vuln [\#705](https://github.com/auth0/java-jwt/pull/705) ([tanya732](https://github.com/tanya732)) +- Fix typo in example code [\#682](https://github.com/auth0/java-jwt/pull/682) ([kasperkarlsson](https://github.com/kasperkarlsson)) +- Remove dead README links [\#676](https://github.com/auth0/java-jwt/pull/676) ([jimmyjames](https://github.com/jimmyjames)) +- Fix typo on a comment in JWTCreator.java [\#672](https://github.com/auth0/java-jwt/pull/672) ([sgc109](https://github.com/sgc109)) +- Remove CircleCI [\#670](https://github.com/auth0/java-jwt/pull/670) ([jimmyjames](https://github.com/jimmyjames)) +- Empty string audience claim should be deserialized as empty string [\#663](https://github.com/auth0/java-jwt/pull/663) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- empty expected audience array should throw InvalidClaimException [\#679](https://github.com/auth0/java-jwt/pull/679) ([jimmyjames](https://github.com/jimmyjames)) + ## [4.5.0](https://github.com/auth0/java-jwt/tree/4.5.0) (2025-01-28) [Full Changelog](https://github.com/auth0/java-jwt/compare/4.4.0...4.5.0)