diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 9bb01639..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: openjdk:8-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) -Z -C $CIRCLE_SHA1 - - 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 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/.github/CODEOWNERS b/.github/CODEOWNERS index c9ff4921..7958e8bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @auth0/dx-sdks-approver +* @auth0/project-dx-sdks-engineer-codeowner 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/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 new file mode 100644 index 00000000..f58e0249 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Auth0 Community + url: https://community.auth0.com + about: Discuss this library in the Auth0 Community forums 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..01e3a621 --- /dev/null +++ b/.github/actions/maven-publish/action.yml @@ -0,0 +1,44 @@ +name: Publish release to Java + +inputs: + java-version: + required: true + ossr-username: + required: true + ossr-token: + required: true + signing-key: + required: true + signing-password: + 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 }} + 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 + 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}} + SIGNING_PASSWORD: ${{ inputs.signing-password}} \ No newline at end of file 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/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/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/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 diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 00000000..ce302cb4 --- /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@v3 + - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # pin@v1.0.6 diff --git a/.github/workflows/java-release.yml b/.github/workflows/java-release.yml new file mode 100644 index 00000000..00771307 --- /dev/null +++ b/.github/workflows/java-release.yml @@ -0,0 +1,91 @@ +name: Create Java and GitHub Release + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + secrets: + ossr-username: + required: true + ossr-token: + 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 + + # 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: + java-version: ${{ inputs.java-version }} + ossr-username: ${{ secrets.ossr-username }} + 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 + 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..2b00e426 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Create GitHub Release + +on: + pull_request: + types: + - closed + workflow_dispatch: + +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 + secrets: + ossr-username: ${{ secrets.OSSR_USERNAME }} + ossr-token: ${{ secrets.OSSR_TOKEN }} + signing-key: ${{ secrets.SIGNING_KEY }} + signing-password: ${{ secrets.SIGNING_PASSWORD }} + github-token: ${{ secrets.GITHUB_TOKEN }} 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 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 }} diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml new file mode 100644 index 00000000..457b6afa --- /dev/null +++ b/.github/workflows/snyk.yml @@ -0,0 +1,39 @@ +name: Snyk + +on: + merge_group: + workflow_dispatch: + pull_request: + 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: + + check: + 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 diff --git a/.gitignore b/.gitignore index 5ab9b34a..e706fbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,11 @@ crashlytics.properties crashlytics-build.properties fabric.properties +# Eclipse IDE +.classpath +.project +.settings/ + ### Java template *.class @@ -76,6 +81,7 @@ hs_err_pid* build/ target/ dependency-reduced-pom.xml +local.properties # Ignore Gradle GUI config gradle-app.setting diff --git a/.shiprc b/.shiprc new file mode 100644 index 00000000..1b83cc62 --- /dev/null +++ b/.shiprc @@ -0,0 +1,8 @@ +{ + "files": { + "README.md": [], + ".version": [], + "lib/build.gradle": ["version = \"{MAJOR}.{MINOR}.{PATCH}\""] + }, + "prefixVersion": false +} \ No newline at end of file diff --git a/.version b/.version new file mode 100644 index 00000000..ae153944 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +4.5.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 21095ff7..b97fab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,288 @@ # 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) + +**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) + +**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) + +**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) + +**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) + +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) + +**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) + +**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) + +**⚠️ 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) + +**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. + +**Added** +- 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)) +- 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)) +- 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** +- 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** +- 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)) +- 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)) +- 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** +- Updated documentation regarding HMAC Key length [\#580](https://github.com/auth0/java-jwt/pull/580) ([poovamraj](https://github.com/poovamraj)) + +**Breaking changes** +- 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)) +- 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) + +**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) + +**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) + +**Deprecated** +- Deprecate ES256K Algorithm [\#543](https://github.com/auth0/java-jwt/pull/543) ([poovamraj](https://github.com/poovamraj)) + +**Fixed** +- fix typos in JWTVerifier#verify docstring [\#526](https://github.com/auth0/java-jwt/pull/526) ([OdunlamiZO](https://github.com/OdunlamiZO)) + +**Security** +- Bump `jackson-databind` dependency to 2.13.2 [\#542](https://github.com/auth0/java-jwt/pull/542) ([evansims](https://github.com/evansims)) + +## [3.18.3](https://github.com/auth0/java-jwt/tree/3.18.3) (2022-01-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.18.2...3.18.3) + +**Security** +- Update jackson dependency [\#523](https://github.com/auth0/java-jwt/pull/523) ([poovamraj](https://github.com/poovamraj)) + +## [3.18.2](https://github.com/auth0/java-jwt/tree/3.18.2) (2021-09-16) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.18.1...3.18.2) + +**Fixed** +- [SDK-2758] Restore withIssuer [\#513](https://github.com/auth0/java-jwt/pull/513) ([jimmyjames](https://github.com/jimmyjames)) +- [SDK-2751] Serialize audience claim when a List [\#512](https://github.com/auth0/java-jwt/pull/512) ([jimmyjames](https://github.com/jimmyjames)) + +## [3.18.1](https://github.com/auth0/java-jwt/tree/3.18.1) (2021-07-06) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.18.0...3.18.1) + +**Fixed** +- Fix min JDK version regression [\#504](https://github.com/auth0/java-jwt/pull/504) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.18.0](https://github.com/auth0/java-jwt/tree/3.18.0) (2021-07-05) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.17.0...3.18.0) + +**Changed** +- Update OSS release plugin version [\#501](https://github.com/auth0/java-jwt/pull/501) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.17.0](https://github.com/auth0/java-jwt/tree/3.17.0) (2021-06-25) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.16.0...3.17.0) + +**Added** +- Add module system support [\#484](https://github.com/auth0/java-jwt/pull/484) ([XakepSDK](https://github.com/XakepSDK)) + +## [3.16.0](https://github.com/auth0/java-jwt/tree/3.16.0) (2021-05-10) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.15.0...3.16.0) + +**Changed** +- Improve Javadoc generation [\#496](https://github.com/auth0/java-jwt/pull/496) ([Marcono1234](https://github.com/Marcono1234)) +- Add package-info.java for internal `impl` package [\#495](https://github.com/auth0/java-jwt/pull/495) ([Marcono1234](https://github.com/Marcono1234)) + +## [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) + +**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) + +**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) + +**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) + +**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) + +**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/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 00000000..995e4c1d --- /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. +``` 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/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..c47b95c2 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,85 @@ +# 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.RegisteredClaims` and `com.auth0.jwt.HeaderParams`. +- `com.auth0.jwt.interfaces.Verification#withAnyOfAudience` no longer provides a default implementation. + +### 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. + +#### `RegisteredClaims` added + +This class contains constants representing the registered 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`. diff --git a/README.md b/README.md index a0685729..9d0ae41c 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,31 @@ +> **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) -# 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) +![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)](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) +[![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) -A Java implementation of [JSON Web Token (JWT) - RFC 7519](https://tools.ietf.org/html/rfc7519). +:books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :computer: [API Reference](#api-reference) :speech_balloon: [Feedback](#feedback) -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. +## 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. -## Installation +## Getting Started -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 +### Requirements -```xml - - com.auth0 - java-jwt - 3.10.3 - -``` +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. -### Gradle +> `java-jwt` is intended for server-side JVM applications. Android applications should use [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android). -```gradle -implementation 'com.auth0:java-jwt:3.10.3' -``` - -## 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 | | :-------------: | :-------------: | :----- | @@ -47,389 +39,99 @@ 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 | -## 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. +> 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) -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. +> :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 +Add the dependency via Maven: -#### 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); +```xml + + com.auth0 + java-jwt + 4.5.0 + ``` -> 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: - -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`. - +or Gradle: -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. +```gradle +implementation 'com.auth0:java-jwt:4.5.0' ``` -### Create and Sign a Token +### Create a JWT -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. +Use `JWT.create()`, configure the claims, and then call `sign(algorithm)` to sign the JWT. -* Example using `HS256` +The example below demonstrates this using the `RS256` signing algorithm: ```java try { - Algorithm algorithm = Algorithm.HMAC256("secret"); + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); String token = JWT.create() .withIssuer("auth0") .sign(algorithm); } catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. + // Invalid Signing configuration / Couldn't convert Claims. } ``` -* Example using `RS256` +### Verify a JWT -```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"); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); -} catch (JWTVerificationException exception){ - //Invalid signature/claims -} -``` +Create a `JWTVerifier` passing the `Algorithm`, and specify any required claim values. -* Example using `RS256` +The following example uses `RS256` to verify the JWT. ```java String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance +DecodedJWT decodedJWT; try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); JWTVerifier verifier = JWT.require(algorithm) + // specify any specific claim validations .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); + // reusable verifier instance + .build(); + + decodedJWT = 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. - - -#### 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 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.: - -```java -BaseVerification verification = (BaseVerification) JWT.require(algorithm) - .acceptLeeway(1) - .acceptExpiresAt(5); -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 + // Invalid signature/claims } ``` -If the token has an invalid syntax or the header or payload are not JSONs, a `JWTDecodeException` will raise. - - -### 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(); -``` - -#### Not Before ("nbf") - -Returns the Not Before value or null if it's not defined in the Payload. - -```java -Date notBefore = jwt.getNotBefore(); -``` - -#### Issued At ("iat") - -Returns the Issued At value or null if it's not defined in the Payload. - -```java -Date issuedAt = jwt.getIssuedAt(); -``` - -#### JWT ID ("jti") - -Returns the JWT ID value or null if it's not defined in the Payload. - -```java -String id = jwt.getId(); -``` - -#### Private Claims - -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"); -``` - -or - -```java -Claim claim = jwt.getClaim("isAdmin"); -``` - -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. - -```java -String token = JWT.create() - .withClaim("name", 123) - .withArrayClaim("array", new Integer[]{1, 2, 3}) - .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 -JWTVerifier verifier = JWT.require(algorithm) - .withClaim("name", 123) - .withArrayClaim("array", 1, 2, 3) - .build(); -DecodedJWT jwt = verifier.verify("my.jwt.token"); -``` - -> 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. -* **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. - -#### 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://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. -* 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). +- [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.

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/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000..b97ccc37 --- /dev/null +++ b/config/checkstyle/checkstyle.xml @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index aac7c9b4..74a5a049 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,24 @@ 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 + +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..a9ad38d3 --- /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', "11") +} +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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf0..e708b1c0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b16c34a..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.1.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/gradlew b/gradlew index b0d6d0ab..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -7,7 +7,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -125,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -154,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +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" "$@" diff --git a/gradlew.bat b/gradlew.bat index 15e1ee37..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -5,7 +5,7 @@ @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem -@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @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="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 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 dcd0d615..83093fc1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,46 +1,143 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + // https://github.com/melix/japicmp-gradle-plugin/issues/36 + classpath 'com.google.guava:guava:31.1-jre' + } +} + 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 'checkstyle' + id 'me.champeau.gradle.japicmp' version '0.4.1' +} + +sourceSets { + jmh { + + } +} + +configurations { + jmhImplementation { + extendsFrom implementation + } } -logger.lifecycle("Using version ${version} for ${group}.${name}") +checkstyle { + toolVersion '10.0' +} +//We are disabling lint checks for tests +tasks.named("checkstyleTest").configure({ + enabled = false +}) +tasks.named("checkstyleJmh").configure({ + enabled = false +}) + +apply from: rootProject.file('gradle/versioning.gradle') + +version = getVersionFromFile() +group = GROUP +logger.lifecycle("Using version ${version} for ${name} group $group") + +import me.champeau.gradle.japicmp.JapicmpTask -oss { - name "java jwt" - repository "java-jwt" - organization "auth0" - description "Java implementation of JSON Web Token (JWT)" +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.from(files(getBaselineJar(project, baselineVersion))) + newClasspath.from(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 + } +} + +ext { + baselineCompareVersion = '4.1.0' + testInJavaVersions = [8, 11, 17, 21] +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) } } compileJava { - sourceCompatibility '1.7' - targetCompatibility '1.7' + exclude 'module-info.java' + // Required to be compatible with JDK 8+ + options.release = 8 +} + +javadoc { + // Exclude internal implementation package from javadoc + excludes = ['com/auth0/jwt/impl', 'module-info.java'] } dependencies { - 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' - testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' - testImplementation 'org.mockito:mockito-core:2.18.3' + 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' + testImplementation 'junit:junit:4.13.2' + 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 { + toolVersion = "0.8.10" } jacocoTestReport { @@ -56,3 +153,92 @@ test { exceptionFormat "short" } } + +task compileModuleInfoJava(type: JavaCompile) { + classpath = files() + source = 'src/main/java/module-info.java' + destinationDir = compileJava.destinationDir + doLast { + def descriptor = new File(compileJava.destinationDir, 'module-info.class') + def dest = new File(compileJava.destinationDir, 'META-INF/versions/9') + ant.move file: descriptor, todir: dest + } + + doFirst { + options.compilerArgs = [ + '--release', '9', + '--module-path', compileJava.classpath.asPath + ] + } +} + +compileTestJava { + options.release = 8 + options.compilerArgs = ["-Xlint:deprecation"] +} + +def testJava8 = tasks.register('testJava8', Test) { + description = 'Runs unit tests on Java 8.' + group = 'verification' + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + }) + shouldRunAfter(tasks.named('test')) +} + +def testJava17 = tasks.register('testJava17', Test) { + description = 'Runs unit tests on Java 17.' + group = 'verification' + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(17) + }) + 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 { + manifest.attributes('Multi-Release': 'true') +} + +compileModuleInfoJava.dependsOn compileJava +classes.dependsOn compileModuleInfoJava + +// 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' +} + +apply from: rootProject.file('gradle/maven-publish.gradle') 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/ClockImpl.java b/lib/src/main/java/com/auth0/jwt/ClockImpl.java deleted file mode 100644 index 45e3edfc..00000000 --- a/lib/src/main/java/com/auth0/jwt/ClockImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.auth0.jwt; - -import com.auth0.jwt.interfaces.Clock; - -import java.util.Date; - -final class ClockImpl implements Clock { - - ClockImpl() { - } - - @Override - public Date getToday() { - return new Date(); - } -} diff --git a/lib/src/main/java/com/auth0/jwt/HeaderParams.java b/lib/src/main/java/com/auth0/jwt/HeaderParams.java new file mode 100644 index 00000000..1107f313 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/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 final String ALGORITHM = "alg"; + + /** + * The content type of the JWT. + */ + public static final String CONTENT_TYPE = "cty"; + + /** + * The media type of the JWT. + */ + public static final String TYPE = "typ"; + + /** + * The key ID of a JWT used to specify the key for signature validation. + */ + public static final String KEY_ID = "kid"; +} diff --git a/lib/src/main/java/com/auth0/jwt/JWT.java b/lib/src/main/java/com/auth0/jwt/JWT.java index ca05deff..696abe40 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 if you have 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,21 +40,23 @@ 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 if you have 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); } /** - * 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) { @@ -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 0b4da54b..bfcb9147 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -3,24 +3,23 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTCreationException; import com.auth0.jwt.exceptions.SignatureGenerationException; -import com.auth0.jwt.impl.ClaimsHolder; -import com.auth0.jwt.impl.PayloadSerializer; -import com.auth0.jwt.impl.PublicClaims; +import com.auth0.jwt.impl.*; 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 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.time.Instant; +import java.util.*; 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. */ @SuppressWarnings("WeakerAccess") public final class JWTCreator { @@ -29,16 +28,26 @@ public final class JWTCreator { private final String headerJson; private final String payloadJson; - private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) throws JWTCreationException { + private static final ObjectMapper mapper; + private static final SimpleModule module; + + static { + module = new SimpleModule(); + module.addSerializer(PayloadClaimsHolder.class, new PayloadSerializer()); + module.addSerializer(HeaderClaimsHolder.class, new HeaderSerializer()); + + mapper = JsonMapper.builder() + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .build() + .registerModule(module); + } + + 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)); + headerJson = mapper.writeValueAsString(new HeaderClaimsHolder(headerClaims)); + payloadJson = mapper.writeValueAsString(new PayloadClaimsHolder(payloadClaims)); } catch (JsonProcessingException e) { throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", e); } @@ -59,17 +68,16 @@ static JWTCreator.Builder init() { */ public static class Builder { private final Map payloadClaims; - private Map headerClaims; + private final Map headerClaims; Builder() { - this.payloadClaims = new HashMap<>(); - this.headerClaims = new HashMap<>(); + this.payloadClaims = new LinkedHashMap<>(); + this.headerClaims = new LinkedHashMap<>(); } /** * 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. @@ -90,15 +98,37 @@ 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, LinkedHashMap.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, 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. */ public Builder withKeyId(String keyId) { - this.headerClaims.put(PublicClaims.KEY_ID, keyId); + this.headerClaims.put(HeaderParams.KEY_ID, keyId); return this; } @@ -109,7 +139,7 @@ public Builder withKeyId(String keyId) { * @return this same Builder instance. */ public Builder withIssuer(String issuer) { - addClaim(PublicClaims.ISSUER, issuer); + addClaim(RegisteredClaims.ISSUER, issuer); return this; } @@ -120,7 +150,7 @@ public Builder withIssuer(String issuer) { * @return this same Builder instance. */ public Builder withSubject(String subject) { - addClaim(PublicClaims.SUBJECT, subject); + addClaim(RegisteredClaims.SUBJECT, subject); return this; } @@ -131,40 +161,79 @@ public Builder withSubject(String subject) { * @return this same Builder instance. */ public Builder withAudience(String... audience) { - addClaim(PublicClaims.AUDIENCE, audience); + addClaim(RegisteredClaims.AUDIENCE, audience); return this; } /** - * Add a specific Expires At ("exp") claim to the Payload. + * Add a specific Expires At ("exp") claim to the payload. The claim will be written as seconds since the epoch. + * Milliseconds will be truncated by rounding down to the nearest second. * * @param expiresAt the Expires At value. * @return this same Builder instance. */ public Builder withExpiresAt(Date expiresAt) { - addClaim(PublicClaims.EXPIRES_AT, expiresAt); + addClaim(RegisteredClaims.EXPIRES_AT, expiresAt); + return this; + } + + /** + * Add a specific Expires At ("exp") claim to the payload. The claim will be written as seconds since the epoch; + * Milliseconds will be truncated by rounding down to the nearest second. + * + * @param expiresAt the Expires At value. + * @return this same Builder instance. + */ + public Builder withExpiresAt(Instant expiresAt) { + addClaim(RegisteredClaims.EXPIRES_AT, expiresAt); return this; } /** - * Add a specific Not Before ("nbf") claim to the Payload. + * Add a specific Not Before ("nbf") claim to the Payload. The claim will be written as seconds since the epoch; + * Milliseconds will be truncated by rounding down to the nearest second. * * @param notBefore the Not Before value. * @return this same Builder instance. */ public Builder withNotBefore(Date notBefore) { - addClaim(PublicClaims.NOT_BEFORE, notBefore); + addClaim(RegisteredClaims.NOT_BEFORE, notBefore); + return this; + } + + /** + * Add a specific Not Before ("nbf") claim to the Payload. The claim will be written as seconds since the epoch; + * Milliseconds will be truncated by rounding down to the nearest second. + * + * @param notBefore the Not Before value. + * @return this same Builder instance. + */ + public Builder withNotBefore(Instant notBefore) { + addClaim(RegisteredClaims.NOT_BEFORE, notBefore); return this; } /** - * Add a specific Issued At ("iat") claim to the Payload. + * Add a specific Issued At ("iat") claim to the Payload. The claim will be written as seconds since the epoch; + * Milliseconds will be truncated by rounding down to the nearest second. * * @param issuedAt the Issued At value. * @return this same Builder instance. */ public Builder withIssuedAt(Date issuedAt) { - addClaim(PublicClaims.ISSUED_AT, issuedAt); + addClaim(RegisteredClaims.ISSUED_AT, issuedAt); + return this; + } + + /** + * Add a specific Issued At ("iat") claim to the Payload. The claim will be written as seconds since the epoch; + * Milliseconds will be truncated by rounding down to the nearest second. + * + * @param issuedAt the Issued At value. + * @return this same Builder instance. + */ + public Builder withIssuedAt(Instant issuedAt) { + addClaim(RegisteredClaims.ISSUED_AT, issuedAt); return this; } @@ -175,7 +244,7 @@ public Builder withIssuedAt(Date issuedAt) { * @return this same Builder instance. */ public Builder withJWTId(String jwtId) { - addClaim(PublicClaims.JWT_ID, jwtId); + addClaim(RegisteredClaims.JWT_ID, jwtId); return this; } @@ -250,7 +319,8 @@ public Builder withClaim(String name, String value) throws IllegalArgumentExcept } /** - * Add a custom Claim value. + * Add a custom Claim value. The claim will be written as seconds since the epoch. + * Milliseconds will be truncated by rounding down to the nearest second. * * @param name the Claim's name. * @param value the Claim's value. @@ -263,6 +333,82 @@ public Builder withClaim(String name, Date value) throws IllegalArgumentExceptio return this; } + /** + * Add a custom Claim value. The claim will be written as seconds since the epoch. + * Milliseconds will be truncated by rounding down to the nearest second. + * + * @param name the Claim's name. + * @param value the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withClaim(String name, Instant value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + return this; + } + + /** + * 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. + * @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 (map != null && !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 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 (list != null && !validateClaim(list)) { + throw new IllegalArgumentException("Expected list containing Map, List, Boolean, Integer, " + + "Long, Double, String and Date"); + } + addClaim(name, list); + 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. * @@ -306,61 +452,95 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE } /** - * Add a custom Map Claim with the given items. + * Add specific Claims to set as the Payload. If the provided map is null then + * nothing is changed. *

- * Accepted nested types are {@linkplain Map} and {@linkplain List} with basic types + * 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. + *

* - * @param name the Claim's name. - * @param map the Claim's key-values. + *

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

+ * + * @param payloadClaims the values to use as Claims in the token's payload. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null, or if the map contents does not validate. + * @throws IllegalArgumentException if any of the claim keys or null, + * or if the values are not of a supported type. */ - public Builder withClaim(String name, Map map) throws IllegalArgumentException { - 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"); + public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { + if (payloadClaims == null) { + return this; } - addClaim(name, map); + + if (!validatePayload(payloadClaims)) { + throw new IllegalArgumentException("Claim values must only be of types Map, List, Boolean, Integer, " + + "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 + for (Map.Entry entry : payloadClaims.entrySet()) { + addClaim(entry.getKey(), entry.getValue()); + } + return this; } /** - * Add a custom List Claim with the given items. + * Add specific Claims to set as the Payload. If the provided json is null then + * nothing is changed. + * *

- * 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. + * If any of the claims are invalid, none will be added. + *

* - * @param name the Claim's name. - * @param list the Claim's list of values. + * @param payloadClaimsJson the values to use as Claims in the token's payload. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null, or if the list contents does not validate. + * @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; + } - public Builder withClaim(String name, List list) throws IllegalArgumentException { - 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"); + try { + Map payloadClaims = mapper.readValue(payloadClaimsJson, LinkedHashMap.class); + return withPayload(payloadClaims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid payload JSON", e); } - addClaim(name, list); - 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 (!isSupportedType(value)) { + return false; + } + } + return true; } 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; } - if (entry.getKey() == null || !(entry.getKey() instanceof String)) { + if (!(entry.getKey() instanceof String)) { return false; } } @@ -370,7 +550,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; } } @@ -388,29 +568,35 @@ 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 == Boolean.class; } /** - * 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 * @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) { 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) { @@ -426,20 +612,19 @@ private void assertNonNull(String name) { } private void addClaim(String name, Object value) { - if (value == null) { - payloadClaims.remove(name); - return; - } payloadClaims.put(name, value); } } private String sign() throws SignatureGenerationException { - String header = Base64.encodeBase64URLSafeString(headerJson.getBytes(StandardCharsets.UTF_8)); - String payload = Base64.encodeBase64URLSafeString(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 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.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 b14c2ac3..cc283095 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -6,16 +6,19 @@ 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.time.Instant; +import java.util.Base64; import java.util.Date; import java.util.List; import java.util.Map; /** * 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 { @@ -35,10 +38,12 @@ 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) { + throw new JWTDecodeException("The input is not a valid base 64 encoded string.", e); } header = converter.parseHeader(headerJson); payload = converter.parsePayload(payloadJson); @@ -89,16 +94,31 @@ public Date getExpiresAt() { return payload.getExpiresAt(); } + @Override + public Instant getExpiresAtAsInstant() { + return payload.getExpiresAtAsInstant(); + } + @Override public Date getNotBefore() { return payload.getNotBefore(); } + @Override + public Instant getNotBeforeAsInstant() { + return payload.getNotBeforeAsInstant(); + } + @Override public Date getIssuedAt() { return payload.getIssuedAt(); } + @Override + public Instant getIssuedAtAsInstant() { + return payload.getIssuedAtAsInstant(); + } + @Override public String getId() { return payload.getId(); diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index 24e109a4..bf180300 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -3,47 +3,62 @@ 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; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.impl.ExpectedCheckHolder; import com.auth0.jwt.interfaces.Verification; +import java.time.Clock; +import java.time.Duration; +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, but also it's 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; - private final Clock clock; + final List expectedChecks; private final JWTParser parser; - JWTVerifier(Algorithm algorithm, Map claims, Clock clock) { + JWTVerifier(Algorithm algorithm, List expectedChecks) { this.algorithm = algorithm; - this.claims = Collections.unmodifiableMap(claims); - this.clock = clock; + this.expectedChecks = Collections.unmodifiableList(expectedChecks); this.parser = new JWTParser(); } /** - * 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 { return new BaseVerification(algorithm); } + /** + * {@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; - private final Map claims; + private final List expectedChecks; private long defaultLeeway; + private final Map customLeeways; private boolean ignoreIssuedAt; + private Clock clock; BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { @@ -51,25 +66,63 @@ public static class BaseVerification implements Verification { } this.algorithm = algorithm; - this.claims = new HashMap<>(); + this.expectedChecks = new ArrayList<>(); + 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); + 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.", RegisteredClaims.ISSUER, claim); + } + return true; + })); return this; } @Override public Verification withSubject(String subject) { - requireClaim(PublicClaims.SUBJECT, subject); + addCheck(RegisteredClaims.SUBJECT, (claim, decodedJWT) -> + verifyNull(claim, subject) || subject.equals(claim.asString())); return this; } @Override public Verification withAudience(String... audience) { - requireClaim(PublicClaims.AUDIENCE, isNullOrEmpty(audience) ? null : Arrays.asList(audience)); + List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); + 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.", + RegisteredClaims.AUDIENCE, claim); + } + return true; + })); + return this; + } + + @Override + public Verification withAnyOfAudience(String... audience) { + List value = isNullOrEmpty(audience) ? null : Arrays.asList(audience); + 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.", + RegisteredClaims.AUDIENCE, claim); + } + return true; + })); return this; } @@ -83,21 +136,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(RegisteredClaims.EXPIRES_AT, leeway); return this; } @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.NOT_BEFORE, leeway); + customLeeways.put(RegisteredClaims.NOT_BEFORE, leeway); return this; } @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.ISSUED_AT, leeway); + customLeeways.put(RegisteredClaims.ISSUED_AT, leeway); return this; } @@ -109,76 +162,118 @@ public Verification ignoreIssuedAt() { @Override public Verification withJWTId(String jwtId) { - requireClaim(PublicClaims.JWT_ID, jwtId); + addCheck(RegisteredClaims.JWT_ID, ((claim, decodedJWT) -> + verifyNull(claim, jwtId) || jwtId.equals(claim.asString()))); + return this; + } + + @Override + public Verification withClaimPresence(String name) throws IllegalArgumentException { + assertNonNull(name); + //since addCheck already checks presence, we just return true + withClaim(name, ((claim, decodedJWT) -> true)); + 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); - requireClaim(name, value); + 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); - requireClaim(name, value); + 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); - requireClaim(name, value); + 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); - requireClaim(name, value); + 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); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asString()))); return this; } @Override public Verification withClaim(String name, Date value) throws IllegalArgumentException { + return withClaim(name, value != null ? value.toInstant() : null); + } + + @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 + addCheck(name, + ((claim, decodedJWT) -> verifyNull(claim, value) + || value.truncatedTo(ChronoUnit.SECONDS).equals(claim.asInstant()))); + return this; + } + + @Override + public Verification withClaim(String name, BiPredicate predicate) + throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + 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); - requireClaim(name, 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); - requireClaim(name, 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); - requireClaim(name, items); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } @Override public JWTVerifier build() { - return this.build(new ClockImpl()); + return this.build(Clock.systemUTC()); } /** @@ -186,11 +281,102 @@ public JWTVerifier build() { * ONLY FOR TEST PURPOSES. * * @param clock the instance that will handle the current time. - * @return a new JWTVerifier instance with a custom Clock. + * @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); + } + + /** + * 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(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(RegisteredClaims.ISSUED_AT, (claim, decodedJWT) -> + assertValidInstantClaim(RegisteredClaims.ISSUED_AT, claim, issuedAtLeeway, false))); + } + } + + 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 = Arrays.asList(claim.as(Object[].class)); + } + List valueArr = Arrays.asList(expectedClaimValue); + return claimArr.containsAll(valueArr); + } + + 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) { + isValid = assertInstantIsFuture(claimVal, leeway, now); + if (!isValid) { + throw new TokenExpiredException(String.format("The Token has expired on %s.", claimVal), claimVal); + } + } else { + isValid = assertInstantIsLessThanOrEqualToNow(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) { + return claimVal == null || now.minus(Duration.ofSeconds(leeway)).isBefore(claimVal); + } + + private boolean assertInstantIsLessThanOrEqualToNow(Instant claimVal, long leeway, Instant now) { + return !(claimVal != null && now.plus(Duration.ofSeconds(leeway)).isBefore(claimVal)); + } + + private boolean assertValidAudienceClaim( + List actualAudience, + List expectedAudience, + boolean shouldContainAll + ) { + if (actualAudience == null || expectedAudience == null) { + return false; + } + + if (shouldContainAll) { + return actualAudience.containsAll(expectedAudience); + } else { + return !Collections.disjoint(actualAudience, expectedAudience); + } } private void assertPositive(long leeway) { @@ -205,43 +391,46 @@ 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 (ignoreIssuedAt) { - claims.remove(PublicClaims.ISSUED_AT); - return; - } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); - } + 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 void requireClaim(String name, Object value) { - if (value == null) { - claims.remove(name); - return; - } - claims.put(name, value); + 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 static boolean isNullOrEmpty(String[] args) { - if (args == null || args.length == 0) { - return true; + private boolean verifyNull(Claim claim, Object value) { + return value == null && claim.isNull(); } - 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; } @@ -250,10 +439,12 @@ 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 it's 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. + * @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 { @@ -266,136 +457,44 @@ 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 it's 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. + * @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 { verifyAlgorithm(jwt, algorithm); algorithm.verify(jwt); - verifyClaims(jwt, claims); + verifyClaims(jwt, expectedChecks); return jwt; } 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 { - 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; + private void verifyClaims(DecodedJWT jwt, List expectedChecks) + throws TokenExpiredException, InvalidClaimException { + for (ExpectedCheckHolder expectedCheck : expectedChecks) { + boolean isValid; + String claimName = expectedCheck.getClaimName(); + Claim claim = jwt.getClaim(claimName); + + isValid = expectedCheck.verify(claim, jwt); + + if (!isValid) { + throw new IncorrectClaimException( + String.format("The Claim '%s' value doesn't match the required one.", claimName), + claimName, + claim + ); } } } - - 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 Date) { - isValid = value.equals(claim.asDate()); - } 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 assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) { - Date today = clock.getToday(); - today.setTime(today.getTime() / 1000 * 1000); // truncate millis - if (shouldBeFuture) { - assertDateIsFuture(date, leeway, today); - } else { - assertDateIsPast(date, leeway, today); - } - } - - private void assertDateIsFuture(Date date, long leeway, Date today) { - today.setTime(today.getTime() - leeway * 1000); - if (date != null && today.after(date)) { - throw new TokenExpiredException(String.format("The Token has expired on %s.", date)); - } - } - - private void assertDateIsPast(Date date, long leeway, Date today) { - today.setTime(today.getTime() + leeway * 1000); - if (date != null && today.before(date)) { - throw new InvalidClaimException(String.format("The Token can't be used before %s.", date)); - } - } - - 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."); - } - } - - 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/RegisteredClaims.java b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java new file mode 100644 index 00000000..c5509716 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/RegisteredClaims.java @@ -0,0 +1,55 @@ +package com.auth0.jwt; + +/** + * Contains constants representing the name of the Registered Claim Names as defined in Section 4.1 of + * RFC 7529 + */ +public final class RegisteredClaims { + + private RegisteredClaims() { + } + + /** + * The "iss" (issuer) claim identifies the principal that issued the JWT. + * Refer RFC 7529 Section 4.1.1 + */ + 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 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 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 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 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 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 final String JWT_ID = "jti"; + +} diff --git a/lib/src/main/java/com/auth0/jwt/TokenUtils.java b/lib/src/main/java/com/auth0/jwt/TokenUtils.java index cb6cff3e..e62b9832 100644 --- a/lib/src/main/java/com/auth0/jwt/TokenUtils.java +++ b/lib/src/main/java/com/auth0/jwt/TokenUtils.java @@ -12,14 +12,36 @@ abstract class TokenUtils { * @throws JWTDecodeException if the Token doesn't have 3 parts. */ static String[] splitToken(String token) throws JWTDecodeException { - 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], ""}; + if (token == null) { + throw new JWTDecodeException("The token is null."); } - if (parts.length != 3) { - throw new JWTDecodeException(String.format("The token was expected to have 3 parts, but got %s.", parts.length)); + + char delimiter = '.'; + + int firstPeriodIndex = token.indexOf(delimiter); + if (firstPeriodIndex == -1) { + throw wrongNumberOfParts(0); + } + + 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/main/java/com/auth0/jwt/algorithms/Algorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java index 24ad025b..248af7c5 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 { @@ -47,9 +48,7 @@ public static Algorithm RSA256(RSAPublicKey publicKey, RSAPrivateKey privateKey) * @param key the key to use in the verify or signing instance. * @return a valid RSA256 Algorithm. * @throws IllegalArgumentException if the Key Provider is null. - * @deprecated use {@link #RSA256(RSAPublicKey, RSAPrivateKey)} or {@link #RSA256(RSAKeyProvider)} */ - @Deprecated public static Algorithm RSA256(RSAKey key) throws IllegalArgumentException { RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; @@ -85,9 +84,7 @@ public static Algorithm RSA384(RSAPublicKey publicKey, RSAPrivateKey privateKey) * @param key the key to use in the verify or signing instance. * @return a valid RSA384 Algorithm. * @throws IllegalArgumentException if the provided Key is null. - * @deprecated use {@link #RSA384(RSAPublicKey, RSAPrivateKey)} or {@link #RSA384(RSAKeyProvider)} */ - @Deprecated public static Algorithm RSA384(RSAKey key) throws IllegalArgumentException { RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; @@ -123,9 +120,7 @@ public static Algorithm RSA512(RSAPublicKey publicKey, RSAPrivateKey privateKey) * @param key the key to use in the verify or signing instance. * @return a valid RSA512 Algorithm. * @throws IllegalArgumentException if the provided Key is null. - * @deprecated use {@link #RSA512(RSAPublicKey, RSAPrivateKey)} or {@link #RSA512(RSAKeyProvider)} */ - @Deprecated public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; @@ -135,62 +130,68 @@ 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 * @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); } /** - * 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. - * @throws IllegalArgumentException if the provided Secret is null. + * @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 + * @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. - * @throws IllegalArgumentException if the provided Secret is null. + * @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 + * @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. + * Ensure the length of the secret is at least 384 bit long + * @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. + * Ensure the length of the secret is at least 512 bit long + * @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); } /** * 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 * @return a valid HMAC512 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -198,6 +199,7 @@ 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". * @@ -227,9 +229,7 @@ public static Algorithm ECDSA256(ECPublicKey publicKey, ECPrivateKey privateKey) * @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 ECDSA256(ECKey key) throws IllegalArgumentException { ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; @@ -265,9 +265,7 @@ public static Algorithm ECDSA384(ECPublicKey publicKey, ECPrivateKey privateKey) * @param key the key to use in the verify or signing instance. * @return a valid ECDSA384 Algorithm. * @throws IllegalArgumentException if the provided Key is null. - * @deprecated use {@link #ECDSA384(ECPublicKey, ECPrivateKey)} or {@link #ECDSA384(ECDSAKeyProvider)} */ - @Deprecated public static Algorithm ECDSA384(ECKey key) throws IllegalArgumentException { ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; @@ -303,9 +301,7 @@ public static Algorithm ECDSA512(ECPublicKey publicKey, ECPrivateKey privateKey) * @param key the key to use in the verify or signing instance. * @return a valid ECDSA512 Algorithm. * @throws IllegalArgumentException if the provided Key is null. - * @deprecated use {@link #ECDSA512(ECPublicKey, ECPrivateKey)} or {@link #ECDSA512(ECDSAKeyProvider)} */ - @Deprecated public static Algorithm ECDSA512(ECKey key) throws IllegalArgumentException { ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; @@ -323,7 +319,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. */ @@ -341,7 +338,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. */ @@ -358,39 +356,43 @@ 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. */ 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); } /** * 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. - * @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..7b8c5c2a 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java @@ -2,99 +2,108 @@ 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; + 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); @@ -106,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); @@ -126,79 +140,66 @@ byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] header } /** - * Verify signature. + * 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. - * @deprecated rather use corresponding method which takes header and payload as separate inputs + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - @Deprecated - 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. - * @deprecated rather use corresponding method which takes header and payload as separate inputs + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. */ - - @Deprecated - 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. - * - * @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); - 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. - * @deprecated rather use corresponding method which takes header and payload as separate inputs + * @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 - 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 12ddca70..b3046097 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -4,14 +4,20 @@ 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.math.BigInteger; 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 + *

+ * This class is thread-safe. + */ class ECDSAAlgorithm extends Algorithm { private final ECDSAKeyProvider keyProvider; @@ -19,7 +25,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."); @@ -29,25 +36,28 @@ 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); } @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."); } - boolean valid = crypto.verifySignatureFor(getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); + validateSignatureStructure(signatureBytes, publicKey); + boolean valid = crypto.verifySignatureFor( + getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), JOSEToDER(signatureBytes)); if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException + | IllegalStateException | IllegalArgumentException e) { throw new SignatureVerificationException(this, e); } } @@ -65,9 +75,8 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene throw new SignatureGenerationException(this, e); } } - + @Override - @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { ECPrivateKey privateKey = keyProvider.getPrivateKey(); @@ -113,36 +122,65 @@ 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; } - //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 format."); + } + + // get R + byte[] rBytes = new byte[ecNumberSize]; + System.arraycopy(joseSignature, 0, rBytes, 0, ecNumberSize); + if (isAllZeros(rBytes)) { + throw new SignatureException("Invalid signature format."); + } + + // get S + byte[] sBytes = new byte[ecNumberSize]; + System.arraycopy(joseSignature, ecNumberSize, sBytes, 0, ecNumberSize); + if (isAllZeros(sBytes)) { + throw new SignatureException("Invalid signature format."); + } + + //moved this check from JOSEToDER method int rPadding = countPadding(joseSignature, 0, ecNumberSize); int sPadding = countPadding(joseSignature, ecNumberSize, joseSignature.length); int rLength = ecNumberSize - rPadding; @@ -153,6 +191,30 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { throw new SignatureException("Invalid JOSE signature format."); } + 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) { + throw new SignatureException("Invalid signature format."); + } + } + + //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) { @@ -195,12 +257,22 @@ byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { 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; } + 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/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java index 5df23a83..0306e7c4 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -3,24 +3,31 @@ 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 + *

+ * This class is thread-safe. + */ class HMACAlgorithm extends Algorithm { private final CryptoHelper crypto; 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"); } - this.secret = secretBytes; + this.secret = Arrays.copyOf(secretBytes, secretBytes.length); this.crypto = crypto; } @@ -42,14 +49,14 @@ static byte[] getSecretBytes(String secret) throws IllegalArgumentException { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { - boolean valid = crypto.verifySignatureFor(getDescription(), secret, jwt.getHeader(), jwt.getPayload(), signatureBytes); + 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); } } @@ -64,7 +71,6 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene } @Override - @Deprecated public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { return crypto.createSignatureFor(getDescription(), secret, contentBytes); 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..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,7 @@ 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 +13,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); } } @@ -25,7 +30,6 @@ public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGene } @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 15cce55a..ca892e60 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -4,22 +4,27 @@ 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; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +/** + * Subclass representing an RSA signing algorithm + *

+ * This class is thread-safe. + */ class RSAAlgorithm extends Algorithm { private final RSAKeyProvider keyProvider; 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."); @@ -34,24 +39,24 @@ 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."); } - 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 | IllegalStateException e) { + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException + | IllegalArgumentException | IllegalStateException e) { throw new SignatureVerificationException(this, e); } } @Override - @Deprecated public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { try { RSAPrivateKey privateKey = keyProvider.getPrivateKey(); @@ -63,7 +68,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/IncorrectClaimException.java b/lib/src/main/java/com/auth0/jwt/exceptions/IncorrectClaimException.java new file mode 100644 index 00000000..712e937b --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/exceptions/IncorrectClaimException.java @@ -0,0 +1,44 @@ +package com.auth0.jwt.exceptions; + +import com.auth0.jwt.interfaces.Claim; + +/** + * This exception is thrown when the expected value is not found while verifying the Claims. + */ +public class IncorrectClaimException extends InvalidClaimException { + private final String claimName; + + private final Claim claimValue; + + /** + * Used internally to construct the IncorrectClaimException which is thrown when there is verification + * failure for a Claim that exists. + * + * @param message The error message + * @param claimName The Claim name for which verification failed + * @param claim The Claim value for which verification failed + */ + public IncorrectClaimException(String message, String claimName, Claim claim) { + super(message); + this.claimName = claimName; + this.claimValue = claim; + } + + /** + * This method can be used to fetch the name for which the Claim verification failed. + * + * @return The claim name for which the verification failed. + */ + public String getClaimName() { + return claimName; + } + + /** + * This method can be used to fetch the value for which the Claim verification failed. + * + * @return The value for which the verification failed + */ + public Claim getClaimValue() { + return claimValue; + } +} \ No newline at end of file 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..c5b8eb64 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/InvalidClaimException.java @@ -1,8 +1,10 @@ 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); } -} +} \ No newline at end of file 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/MissingClaimException.java b/lib/src/main/java/com/auth0/jwt/exceptions/MissingClaimException.java new file mode 100644 index 00000000..3bcc2121 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/exceptions/MissingClaimException.java @@ -0,0 +1,23 @@ +package com.auth0.jwt.exceptions; + +/** + * This exception is thrown when the claim to be verified is missing. + */ +public class MissingClaimException extends InvalidClaimException { + + private final String claimName; + + public MissingClaimException(String claimName) { + super(String.format("The Claim '%s' is not present in the JWT.", claimName)); + this.claimName = claimName; + } + + /** + * This method can be used to fetch the name for which the Claim is missing during the verification. + * + * @return The name of the Claim that doesn't exist. + */ + public String getClaimName() { + return claimName; + } +} 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..42ab090d 100644 --- a/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java +++ b/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java @@ -1,10 +1,22 @@ package com.auth0.jwt.exceptions; +import java.time.Instant; + +/** + * The exception that is thrown if the token is expired. + */ public class TokenExpiredException extends JWTVerificationException { private static final long serialVersionUID = -7076928975713577708L; - public TokenExpiredException(String message) { + private final Instant expiredOn; + + public TokenExpiredException(String message, Instant expiredOn) { super(message); + this.expiredOn = expiredOn; + } + + public Instant getExpiredOn() { + return expiredOn; } } 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..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,15 +22,22 @@ class BasicHeader implements Header, Serializable { private final String contentType; 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) { + private final ObjectCodec objectCodec; + + BasicHeader( + String algorithm, + String type, + String contentType, + String keyId, + Map tree, + 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() { @@ -60,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/ClaimsHolder.java b/lib/src/main/java/com/auth0/jwt/impl/ClaimsHolder.java index 17602199..30f6ab18 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/ClaimsHolder.java +++ b/lib/src/main/java/com/auth0/jwt/impl/ClaimsHolder.java @@ -6,11 +6,11 @@ /** * The ClaimsHolder class is just a wrapper for the Map of Claims used for building a JWT. */ -public final class ClaimsHolder { +public abstract class ClaimsHolder { private Map claims; - public ClaimsHolder(Map claims) { - this.claims = claims == null ? new HashMap() : claims; + protected ClaimsHolder(Map claims) { + this.claims = claims == null ? new HashMap<>() : claims; } Map getClaims() { diff --git a/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java new file mode 100644 index 00000000..b1f8e6d3 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/ClaimsSerializer.java @@ -0,0 +1,86 @@ +package com.auth0.jwt.impl; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import java.io.IOException; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Custom serializer used to write the resulting JWT. + * + * @param the type this serializer operates on. + */ +public class ClaimsSerializer extends StdSerializer { + + public ClaimsSerializer(Class t) { + super(t); + } + + @Override + public void serialize(T holder, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : holder.getClaims().entrySet()) { + writeClaim(entry, gen); + } + gen.writeEndObject(); + } + + /** + * 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. + * + * @param entry The entry that corresponds to the JSON field to write + * @param gen The {@code JsonGenerator} to use + * @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()); + handleSerialization(entry.getValue(), gen); + } + + private static void handleSerialization(Object value, JsonGenerator gen) throws IOException { + if (value instanceof Date) { + gen.writeNumber(dateToSeconds((Date) value)); + } else if (value instanceof Instant) { // EXPIRES_AT, ISSUED_AT, NOT_BEFORE, custom Instant claims + gen.writeNumber(instantToSeconds((Instant) value)); + } else if (value instanceof Map) { + serializeMap((Map) value, gen); + } else if (value instanceof List) { + serializeList((List) value, gen); + } else { + gen.writeObject(value); + } + } + + private static void serializeMap(Map map, JsonGenerator gen) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : map.entrySet()) { + gen.writeFieldName((String) entry.getKey()); + Object value = entry.getValue(); + handleSerialization(value, gen); + } + gen.writeEndObject(); + } + + private static void serializeList(List list, JsonGenerator gen) throws IOException { + gen.writeStartArray(); + for (Object entry : list) { + handleSerialization(entry, gen); + } + gen.writeEndArray(); + } + + private static long instantToSeconds(Instant instant) { + return instant.getEpochSecond(); + } + + private static long dateToSeconds(Date date) { + return date.getTime() / 1000; + } +} 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/main/java/com/auth0/jwt/impl/HeaderClaimsHolder.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderClaimsHolder.java new file mode 100644 index 00000000..9b480116 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderClaimsHolder.java @@ -0,0 +1,12 @@ +package com.auth0.jwt.impl; + +import java.util.Map; + +/** + * Holds the header claims when serializing a JWT. + */ +public final class HeaderClaimsHolder extends ClaimsHolder { + public HeaderClaimsHolder(Map claims) { + super(claims); + } +} 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..ad6e4ce0 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderDeserializer.java @@ -1,43 +1,43 @@ package com.auth0.jwt.impl; +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; import java.util.Map; -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; +/** + * Jackson deserializer implementation for converting from JWT Header parts. + *

+ * This class is thread-safe. + * + * @see JWTParser + */ +class HeaderDeserializer extends StdDeserializer

{ + + 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) { 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); - return new BasicHeader(algorithm, type, contentType, keyId, tree, objectReader); + 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, p.getCodec()); } String getString(Map tree, String claimName) { diff --git a/lib/src/main/java/com/auth0/jwt/impl/HeaderSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/HeaderSerializer.java new file mode 100644 index 00000000..5c7cf0fc --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/HeaderSerializer.java @@ -0,0 +1,10 @@ +package com.auth0.jwt.impl; + +/** + * Responsible for serializing a JWT's header representation to JSON. + */ +public class HeaderSerializer extends ClaimsSerializer { + public HeaderSerializer() { + super(HeaderClaimsHolder.class); + } +} 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..022520f5 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java @@ -9,19 +9,28 @@ 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 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); } @@ -52,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 4e2aef63..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,13 +4,13 @@ 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.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; import java.io.IOException; import java.lang.reflect.Array; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -21,78 +21,87 @@ */ 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 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(); return new Date(seconds * 1000); } + @Override + public Instant asInstant() { + if (isMissing() || isNull() || !data.canConvertToLong()) { + return null; + } + long seconds = data.asLong(); + return Instant.ofEpochSecond(seconds); + } + @Override @SuppressWarnings("unchecked") - public T[] asArray(Class tClazz) throws JWTDecodeException { - if (!data.isArray()) { + public T[] asArray(Class clazz) throws JWTDecodeException { + if (isMissing() || isNull() || !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] = codec.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 { - if (!data.isArray()) { + public List asList(Class clazz) throws JWTDecodeException { + if (isMissing() || isNull() || !data.isArray()) { return null; } List list = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { try { - list.add(objectReader.treeToValue(data.get(i), tClazz)); + list.add(codec.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; @@ -100,32 +109,50 @@ public List asList(Class tClazz) throws JWTDecodeException { @Override public Map asMap() throws JWTDecodeException { - if (!data.isObject()) { + if (isMissing() || isNull() || !data.isObject()) { 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); } } @Override - public T as(Class tClazz) throws JWTDecodeException { + public T as(Class clazz) throws JWTDecodeException { try { - return objectReader.treeAsTokens(data).readValueAs(tClazz); - } catch (IOException e) { - throw new JWTDecodeException("Couldn't map the Claim value to " + tClazz.getSimpleName(), e); + if (isMissing() || isNull()) { + return null; + } + return codec.treeToValue(data, clazz); + } catch (JsonProcessingException e) { + throw new JWTDecodeException("Couldn't map the Claim value to " + clazz.getSimpleName(), e); } } @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(); } /** @@ -133,24 +160,23 @@ public boolean isNull() { * * @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) { - if (node == null || node.isNull() || node.isMissingNode()) { - return new NullClaim(); - } - 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/NullClaim.java b/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java deleted file mode 100644 index 93bedbb1..00000000 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.auth0.jwt.impl; - -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.Claim; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** - * The {@link NullClaim} class is a Claim implementation that returns null when any of it's methods it's 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 T[] asArray(Class tClazz) throws JWTDecodeException { - return null; - } - - @Override - public List asList(Class tClazz) throws JWTDecodeException { - return null; - } - - @Override - public Map asMap() throws JWTDecodeException { - return null; - } - - @Override - public T as(Class tClazz) throws JWTDecodeException { - return null; - } -} diff --git a/lib/src/main/java/com/auth0/jwt/impl/PayloadClaimsHolder.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadClaimsHolder.java new file mode 100644 index 00000000..7055a2ce --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadClaimsHolder.java @@ -0,0 +1,12 @@ +package com.auth0.jwt.impl; + +import java.util.Map; + +/** + * Holds the payload claims when serializing a JWT. + */ +public final class PayloadClaimsHolder extends ClaimsHolder { + public PayloadClaimsHolder(Map claims) { + super(claims); + } +} 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..b1d32a12 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadDeserializer.java @@ -1,9 +1,11 @@ package com.auth0.jwt.impl; +import com.auth0.jwt.RegisteredClaims; import com.auth0.jwt.exceptions.JWTDecodeException; 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; @@ -11,20 +13,20 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; +import java.time.Instant; import java.util.*; +/** + * Jackson deserializer implementation for converting from JWT Payload parts. + *

+ * This class is thread-safe. + * + * @see JWTParser + */ 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 @@ -35,30 +37,31 @@ 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); - Date expiresAt = getDateFromSeconds(tree, PublicClaims.EXPIRES_AT); - Date notBefore = getDateFromSeconds(tree, PublicClaims.NOT_BEFORE); - Date issuedAt = getDateFromSeconds(tree, PublicClaims.ISSUED_AT); - String jwtId = getString(tree, PublicClaims.JWT_ID); + String issuer = getString(tree, RegisteredClaims.ISSUER); + String subject = getString(tree, RegisteredClaims.SUBJECT); + 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; } - if (node.isTextual() && !node.asText().isEmpty()) { + if (node.isTextual()) { return Collections.singletonList(node.asText()); } 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); } @@ -66,16 +69,16 @@ List getStringOrArray(Map tree, String claimName) thro return list; } - Date getDateFromSeconds(Map tree, String claimName) { + Instant getInstantFromSeconds(Map tree, String claimName) { JsonNode node = tree.get(claimName); if (node == null || node.isNull()) { 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)); } - final long ms = node.asLong() * 1000; - return new Date(ms); + return Instant.ofEpochSecond(node.asLong()); } String getString(Map tree, String claimName) { 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..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,16 +2,25 @@ 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.util.*; +import java.time.Instant; +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; /** - * The PayloadImpl class implements the Payload interface. + * Decoder of string JSON Web Tokens into their POJO representations. + *

+ * This class is thread-safe. + * + * @see Payload */ class PayloadImpl implements Payload, Serializable { @@ -20,23 +29,33 @@ class PayloadImpl implements Payload, Serializable { private final String issuer; private final String subject; private final List audience; - private final Date expiresAt; - private final Date notBefore; - private final Date issuedAt; + private final Instant expiresAt; + private final Instant notBefore; + private final Instant 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, ObjectReader objectReader) { + private final ObjectCodec objectCodec; + + PayloadImpl( + String issuer, + String subject, + List audience, + Instant expiresAt, + Instant notBefore, + Instant issuedAt, + String jwtId, + Map tree, + ObjectCodec objectCodec + ) { 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; 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() { @@ -60,19 +79,35 @@ public List getAudience() { @Override public Date getExpiresAt() { - return expiresAt; + return (expiresAt != null) ? Date.from(expiresAt) : null; } + @Override - public Date getNotBefore() { - return notBefore; + public Instant getExpiresAtAsInstant() { + return expiresAt; } @Override public Date getIssuedAt() { + return (issuedAt != null) ? Date.from(issuedAt) : null; + } + + @Override + public Instant getIssuedAtAsInstant() { return issuedAt; } + @Override + public Date getNotBefore() { + return (notBefore != null) ? Date.from(notBefore) : null; + } + + @Override + public Instant getNotBeforeAsInstant() { + return notBefore; + } + @Override public String getId() { return jwtId; @@ -80,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/main/java/com/auth0/jwt/impl/PayloadSerializer.java b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java index dd0d2f42..24fe37b7 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java +++ b/lib/src/main/java/com/auth0/jwt/impl/PayloadSerializer.java @@ -1,63 +1,66 @@ package com.auth0.jwt.impl; +import com.auth0.jwt.RegisteredClaims; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; import java.io.IOException; -import java.util.Date; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Map; -public class PayloadSerializer extends StdSerializer { - +/** + * Jackson serializer implementation for converting into JWT Payload parts. + *

+ * This class is thread-safe. + * + * @see com.auth0.jwt.JWTCreator + */ +public class PayloadSerializer extends ClaimsSerializer { public PayloadSerializer() { - this(null); + super(PayloadClaimsHolder.class); } - private PayloadSerializer(Class t) { - super(t); + @Override + protected void writeClaim(Map.Entry entry, JsonGenerator gen) throws IOException { + if (RegisteredClaims.AUDIENCE.equals(entry.getKey())) { + writeAudience(gen, entry); + } else { + super.writeClaim(entry, gen); + } } - @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()); - break; - } - String[] audArray = (String[]) e.getValue(); - if (audArray.length == 1) { - gen.writeFieldName(e.getKey()); - gen.writeString(audArray[0]); - } else if (audArray.length > 1) { - gen.writeFieldName(e.getKey()); - gen.writeStartArray(); - for(String aud : audArray) { - gen.writeString(aud); - } - gen.writeEndArray(); + /** + * 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()); + gen.writeString((String) e.getValue()); + } else { + List audArray = new ArrayList<>(); + if (e.getValue() instanceof String[]) { + audArray = Arrays.asList((String[]) e.getValue()); + } else if (e.getValue() instanceof List) { + List audList = (List) e.getValue(); + for (Object aud : audList) { + if (aud instanceof String) { + audArray.add((String) aud); } - break; - default: - gen.writeFieldName(e.getKey()); - if (e.getValue() instanceof Date) { // true for EXPIRES_AT, ISSUED_AT, NOT_BEFORE - gen.writeNumber(dateToSeconds((Date) e.getValue())); - } else { - gen.writeObject(e.getValue()); - } - break; + } + } + if (audArray.size() == 1) { + gen.writeFieldName(e.getKey()); + gen.writeString(audArray.get(0)); + } else if (audArray.size() > 1) { + gen.writeFieldName(e.getKey()); + gen.writeStartArray(); + for (String aud : audArray) { + gen.writeString(aud); + } + gen.writeEndArray(); } } - - gen.writeEndObject(); - } - - private long dateToSeconds(Date date) { - return date.getTime() / 1000; } } 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 cc2b3db8..00000000 --- a/lib/src/main/java/com/auth0/jwt/impl/PublicClaims.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.auth0.jwt.impl; - - -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/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/Claim.java b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java index b3d41b1d..ca5244d6 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Claim.java @@ -2,6 +2,7 @@ import com.auth0.jwt.exceptions.JWTDecodeException; +import java.time.Instant; import java.util.Date; import java.util.List; import java.util.Map; @@ -13,14 +14,23 @@ 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 {@code 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. + * 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. */ @@ -28,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. */ @@ -36,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. */ @@ -44,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. */ @@ -52,40 +62,53 @@ 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, {@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(); /** * 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. */ Date asDate(); + /** + * Get this Claim as an Instant. + * If the value can't be converted to an Instant, {@code null} will be returned. + * + * @return the value as a Date or null. + */ + default Instant asInstant() { + Date date = asDate(); + return date != null ? date.toInstant() : null; + } + /** * 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 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. + * If the value isn't an Array, {@code 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. @@ -97,11 +120,12 @@ public interface Claim { /** * 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 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/Clock.java b/lib/src/main/java/com/auth0/jwt/interfaces/Clock.java deleted file mode 100644 index 7dd9c43b..00000000 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Clock.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.auth0.jwt.interfaces; - -import java.util.Date; - -/** - * The Clock class is used to wrap calls to Date class. - */ -public interface Clock { - - /** - * Returns a new Date representing Today's time. - * - * @return a new Date representing Today's time. - */ - Date getToday(); -} 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..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 { @@ -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 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 520e35c8..33cd0d70 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTPartsParser.java @@ -3,12 +3,13 @@ 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 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. @@ -17,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 a335a83e..2756ddd8 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,25 @@ import com.auth0.jwt.exceptions.JWTVerificationException; +/** + * 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 { /** - * 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 +30,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/KeyProvider.java b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java index fa3b13c9..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 @@ -14,7 +15,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 +29,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/Payload.java b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java index 0f639ab9..feb58c64 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Payload.java @@ -1,11 +1,12 @@ package com.auth0.jwt.interfaces; +import java.time.Instant; import java.util.Date; import java.util.List; 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 { @@ -37,6 +38,15 @@ public interface Payload { */ Date getExpiresAt(); + /** + * Get the value of the "exp" claim as an {@linkplain Instant}, or null if it's not available. + * + * @return the Expiration Time value or null. + */ + default Instant getExpiresAtAsInstant() { + return getExpiresAt() != null ? getExpiresAt().toInstant() : null; + } + /** * Get the value of the "nbf" claim, or null if it's not available. * @@ -44,6 +54,15 @@ public interface Payload { */ Date getNotBefore(); + /** + * Get the value of the "nbf" claim as an {@linkplain Instant}, or null if it's not available. + * + * @return the Not Before value or null. + */ + default Instant getNotBeforeAsInstant() { + return getNotBefore() != null ? getNotBefore().toInstant() : null; + } + /** * Get the value of the "iat" claim, or null if it's not available. * @@ -51,6 +70,15 @@ public interface Payload { */ Date getIssuedAt(); + /** + * Get the value of the "iat" claim as an {@linkplain Instant}, or null if it's not available. + * + * @return the Issued At value or null. + */ + default Instant getIssuedAtAsInstant() { + return getIssuedAt() != null ? getIssuedAt().toInstant() : null; + } + /** * Get the value of the "jti" claim, or null if it's not available. * @@ -59,7 +87,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 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 72ae35d2..b4adcf5c 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -2,14 +2,31 @@ import com.auth0.jwt.JWTVerifier; +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. + * 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 { + + /** + * 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. + */ + default Verification withIssuer(String issuer) { + return withIssuer(new 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. @@ -17,7 +34,8 @@ public interface Verification { 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. @@ -25,7 +43,8 @@ public interface Verification { Verification withSubject(String subject); /** - * Require a specific Audience ("aud") claim. + * 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. @@ -33,8 +52,17 @@ public interface Verification { 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. + * 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. + */ + 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. * * @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. @@ -44,7 +72,8 @@ public interface Verification { /** * 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. @@ -54,7 +83,8 @@ public interface Verification { /** * 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. @@ -64,7 +94,11 @@ public interface Verification { /** * 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 + * 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. * * @param leeway the window in seconds in which the Issued At Claim will still be valid. * @return this same Verification instance. @@ -73,106 +107,152 @@ public interface Verification { 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 specific Claim 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 {@code null}. + */ + Verification withClaimPresence(String name) throws IllegalArgumentException; + + /** + * 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 {@code null}. + */ + Verification withNullClaim(String name) throws IllegalArgumentException; + + /** + * 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. + * 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 Array Claim to contain at least the given items. + * 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 {@code null}. + */ + default Verification withClaim(String name, Instant value) throws IllegalArgumentException { + return withClaim(name, value != null ? Date.from(value) : null); + } + + /** + * 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 {@code null}. + */ + Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException; + + /** + * 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. */ @@ -181,7 +261,7 @@ public interface Verification { /** * 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/main/java/module-info.java b/lib/src/main/java/module-info.java new file mode 100644 index 00000000..a1e30e31 --- /dev/null +++ b/lib/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module com.auth0.jwt { + requires com.fasterxml.jackson.databind; + + exports com.auth0.jwt; + exports com.auth0.jwt.algorithms; + exports com.auth0.jwt.exceptions; + exports com.auth0.jwt.interfaces; +} diff --git a/lib/src/test/java/com/auth0/jwt/ClockImplTest.java b/lib/src/test/java/com/auth0/jwt/ClockImplTest.java deleted file mode 100644 index 0eec07d7..00000000 --- a/lib/src/test/java/com/auth0/jwt/ClockImplTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.auth0.jwt; - -import com.auth0.jwt.interfaces.Clock; -import org.junit.Test; - -import java.util.Date; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.*; - -public class ClockImplTest { - - @Test - public void shouldGetToday() throws Exception{ - Clock clock = new ClockImpl(); - Date clockToday = clock.getToday(); - assertThat(clockToday, is(notNullValue())); - } - -} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java index 41098c25..32ede1de 100644 --- a/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java +++ b/lib/src/test/java/com/auth0/jwt/ConcurrentVerifyTest.java @@ -33,12 +33,12 @@ public class ConcurrentVerifyTest { private static ExecutorService executor; @BeforeClass - public static void beforeAll() throws Exception { + public static void beforeAll() { executor = Executors.newFixedThreadPool(THREAD_COUNT); } @AfterClass - public static void afterAll() throws Exception { + public static void afterAll() { executor.shutdown(); } @@ -63,7 +63,7 @@ private static class VerifyTask implements Callable { } @Override - public DecodedJWT call() throws Exception { + public DecodedJWT call() { DecodedJWT jwt = null; try { jwt = verifier.verify(token); @@ -84,7 +84,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 +139,7 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { 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..53cd267b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -1,11 +1,10 @@ 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; -import org.apache.commons.codec.binary.Base64; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -13,12 +12,12 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; +import java.time.Instant; 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; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -27,46 +26,109 @@ 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"; - @Rule public ExpectedException exception = ExpectedException.none(); @Test - public void shouldThrowWhenRequestingSignWithoutAlgorithm() throws Exception { + public void shouldThrowWhenRequestingSignWithoutAlgorithm() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Algorithm cannot be null"); JWTCreator.init() .sign(null); } - @SuppressWarnings("Convert2Diamond") @Test - public void shouldAddHeaderClaim() throws Exception { - Map header = new HashMap(); - header.put("asd", 123); + public void shouldAddHeaderClaim() { + Date date = new Date(123000); + Instant instant = date.toInstant(); + + List list = Arrays.asList(date, instant); + Map map = new HashMap<>(); + map.put("date", date); + map.put("instant", instant); + + List expectedSerializedList = Arrays.asList(date.getTime() / 1000, instant.getEpochSecond()); + Map expectedSerializedMap = new HashMap<>(); + expectedSerializedMap.put("date", date.getTime() / 1000); + expectedSerializedMap.put("instant", instant.getEpochSecond()); + + Map header = new HashMap<>(); + header.put("string", "string"); + header.put("int", 42); + header.put("long", 4200000000L); + header.put("double", 123.123); + header.put("bool", true); + header.put("date", date); + header.put("instant", instant); + header.put("list", list); + header.put("map", map); + 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("asd", 123)); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("string", "string")); + assertThat(headerJson, JsonMatcher.hasEntry("int", 42)); + assertThat(headerJson, JsonMatcher.hasEntry("long", 4200000000L)); + assertThat(headerJson, JsonMatcher.hasEntry("double", 123.123)); + assertThat(headerJson, JsonMatcher.hasEntry("bool", true)); + assertThat(headerJson, JsonMatcher.hasEntry("date", 123)); + assertThat(headerJson, JsonMatcher.hasEntry("instant", 123)); + assertThat(headerJson, JsonMatcher.hasEntry("list", expectedSerializedList)); + assertThat(headerJson, JsonMatcher.hasEntry("map", expectedSerializedMap)); } @Test - public void shouldReturnBuilderIfNullMapIsProvided() throws Exception { + 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 shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() throws Exception { - Map header = new HashMap(); - header.put(PublicClaims.KEY_ID, "xyz"); + 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<>(); + header.put(HeaderParams.KEY_ID, "xyz"); String signed = JWTCreator.init() .withKeyId("abc") @@ -75,14 +137,15 @@ public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() throws 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, "xyz")); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } + @Test - public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() throws Exception { - Map header = new HashMap(); - header.put(PublicClaims.KEY_ID, "xyz"); + public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() { + Map header = new HashMap<>(); + header.put(HeaderParams.KEY_ID, "xyz"); String signed = JWTCreator.init() .withHeader(header) @@ -91,14 +154,14 @@ public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() throws Exce 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")); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "abc")); } @Test - public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { - Map header = new HashMap(); - header.put(PublicClaims.KEY_ID, null); + public void shouldRemoveHeaderIfTheValueIsNull() { + Map header = new HashMap<>(); + header.put(HeaderParams.KEY_ID, null); header.put("test2", "isSet"); String signed = JWTCreator.init() @@ -108,20 +171,20 @@ public void shouldRemoveHeaderIfTheValueIsNull() throws Exception { assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.isNotPresent(PublicClaims.KEY_ID)); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.isNotPresent(HeaderParams.KEY_ID)); assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet")); } @Test - public void shouldAddKeyId() throws Exception { + public void shouldAddKeyId() { String signed = JWTCreator.init() .withKeyId("56a8bd44da435300010000015f5ed") .sign(Algorithm.HMAC256("secret")); 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")); } @@ -137,7 +200,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")); } @@ -154,7 +217,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")); } @@ -170,7 +233,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")); } @@ -187,12 +250,12 @@ 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")); } @Test - public void shouldAddIssuer() throws Exception { + public void shouldAddIssuer() { String signed = JWTCreator.init() .withIssuer("auth0") .sign(Algorithm.HMAC256("secret")); @@ -202,7 +265,7 @@ public void shouldAddIssuer() throws Exception { } @Test - public void shouldAddSubject() throws Exception { + public void shouldAddSubject() { String signed = JWTCreator.init() .withSubject("1234567890") .sign(Algorithm.HMAC256("secret")); @@ -212,7 +275,7 @@ public void shouldAddSubject() throws Exception { } @Test - public void shouldAddAudience() throws Exception { + public void shouldAddAudience() { String signed = JWTCreator.init() .withAudience("Mark") .sign(Algorithm.HMAC256("secret")); @@ -230,7 +293,7 @@ public void shouldAddAudience() throws Exception { } @Test - public void shouldAddExpiresAt() throws Exception { + public void shouldAddExpiresAt() { String signed = JWTCreator.init() .withExpiresAt(new Date(1477592000)) .sign(Algorithm.HMAC256("secret")); @@ -240,7 +303,18 @@ public void shouldAddExpiresAt() throws Exception { } @Test - public void shouldAddNotBefore() throws Exception { + public void shouldAddExpiresAtInstant() { + String signed = JWTCreator.init() + .withExpiresAt(Instant.ofEpochSecond(1477592)) + .sign(Algorithm.HMAC256("secret")); + + System.out.println(signed); + assertThat(signed, is(notNullValue())); + assertThat(TokenUtils.splitToken(signed)[1], is("eyJleHAiOjE0Nzc1OTJ9")); + } + + @Test + public void shouldAddNotBefore() { String signed = JWTCreator.init() .withNotBefore(new Date(1477592000)) .sign(Algorithm.HMAC256("secret")); @@ -250,7 +324,17 @@ public void shouldAddNotBefore() throws Exception { } @Test - public void shouldAddIssuedAt() throws Exception { + public void shouldAddNotBeforeInstant() { + String signed = JWTCreator.init() + .withNotBefore(Instant.ofEpochSecond(1477592)) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + assertThat(TokenUtils.splitToken(signed)[1], is("eyJuYmYiOjE0Nzc1OTJ9")); + } + + @Test + public void shouldAddIssuedAt() { String signed = JWTCreator.init() .withIssuedAt(new Date(1477592000)) .sign(Algorithm.HMAC256("secret")); @@ -260,50 +344,49 @@ public void shouldAddIssuedAt() throws Exception { } @Test - public void shouldAddJWTId() throws Exception { + public void shouldAddIssuedAtInstant() { String signed = JWTCreator.init() - .withJWTId("jwt_id_123") + .withIssuedAt(Instant.ofEpochSecond(1477592)) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); - assertThat(TokenUtils.splitToken(signed)[1], is("eyJqdGkiOiJqd3RfaWRfMTIzIn0")); + assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9")); } @Test - public void shouldRemoveClaimWhenPassingNull() throws Exception { + public void shouldAddJWTId() { String signed = JWTCreator.init() - .withIssuer("iss") - .withIssuer(null) + .withJWTId("jwt_id_123") .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); - assertThat(TokenUtils.splitToken(signed)[1], is("e30")); + assertThat(TokenUtils.splitToken(signed)[1], is("eyJqdGkiOiJqd3RfaWRfMTIzIn0")); } @Test - public void shouldSetCorrectAlgorithmInTheHeader() throws Exception { + public void shouldSetCorrectAlgorithmInTheHeader() { String signed = JWTCreator.init() .sign(Algorithm.HMAC256("secret")); 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")); } @Test - public void shouldSetDefaultTypeInTheHeader() throws Exception { + public void shouldSetDefaultTypeInTheHeader() { String signed = JWTCreator.init() .sign(Algorithm.HMAC256("secret")); 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")); } @Test - public void shouldSetCustomTypeInTheHeader() throws Exception { + public void shouldSetCustomTypeInTheHeader() { Map header = Collections.singletonMap("typ", "passport"); String signed = JWTCreator.init() .withHeader(header) @@ -311,12 +394,12 @@ 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")); } @Test - public void shouldSetEmptySignatureIfAlgorithmIsNone() throws Exception { + public void shouldSetEmptySignatureIfAlgorithmIsNone() { String signed = JWTCreator.init() .sign(Algorithm.none()); assertThat(signed, is(notNullValue())); @@ -324,7 +407,7 @@ public void shouldSetEmptySignatureIfAlgorithmIsNone() throws Exception { } @Test - public void shouldThrowOnNullCustomClaimName() throws Exception { + public void shouldThrowOnNullCustomClaimName() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Custom Claim's name can't be null."); JWTCreator.init() @@ -332,7 +415,7 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeString() throws Exception { + public void shouldAcceptCustomClaimOfTypeString() { String jwt = JWTCreator.init() .withClaim("name", "value") .sign(Algorithm.HMAC256("secret")); @@ -343,7 +426,7 @@ public void shouldAcceptCustomClaimOfTypeString() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeInteger() throws Exception { + public void shouldAcceptCustomClaimOfTypeInteger() { String jwt = JWTCreator.init() .withClaim("name", 123) .sign(Algorithm.HMAC256("secret")); @@ -354,7 +437,7 @@ public void shouldAcceptCustomClaimOfTypeInteger() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeLong() throws Exception { + public void shouldAcceptCustomClaimOfTypeLong() { String jwt = JWTCreator.init() .withClaim("name", Long.MAX_VALUE) .sign(Algorithm.HMAC256("secret")); @@ -365,7 +448,7 @@ public void shouldAcceptCustomClaimOfTypeLong() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeDouble() throws Exception { + public void shouldAcceptCustomClaimOfTypeDouble() { String jwt = JWTCreator.init() .withClaim("name", 23.45) .sign(Algorithm.HMAC256("secret")); @@ -376,7 +459,7 @@ public void shouldAcceptCustomClaimOfTypeDouble() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeBoolean() throws Exception { + public void shouldAcceptCustomClaimOfTypeBoolean() { String jwt = JWTCreator.init() .withClaim("name", true) .sign(Algorithm.HMAC256("secret")); @@ -387,7 +470,7 @@ public void shouldAcceptCustomClaimOfTypeBoolean() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeDate() throws Exception { + public void shouldAcceptCustomClaimOfTypeDate() { Date date = new Date(1478891521000L); String jwt = JWTCreator.init() .withClaim("name", date) @@ -399,7 +482,19 @@ public void shouldAcceptCustomClaimOfTypeDate() throws Exception { } @Test - public void shouldAcceptCustomArrayClaimOfTypeString() throws Exception { + public void shouldAcceptCustomClaimOfTypeDateInstant() { + Instant instant = Instant.ofEpochSecond(1478891521); + String jwt = JWTCreator.init() + .withClaim("name", instant) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjoxNDc4ODkxNTIxfQ")); + } + + @Test + public void shouldAcceptCustomArrayClaimOfTypeString() { String jwt = JWTCreator.init() .withArrayClaim("name", new String[]{"text", "123", "true"}) .sign(Algorithm.HMAC256("secret")); @@ -410,7 +505,7 @@ public void shouldAcceptCustomArrayClaimOfTypeString() throws Exception { } @Test - public void shouldAcceptCustomArrayClaimOfTypeInteger() throws Exception { + public void shouldAcceptCustomArrayClaimOfTypeInteger() { String jwt = JWTCreator.init() .withArrayClaim("name", new Integer[]{1, 2, 3}) .sign(Algorithm.HMAC256("secret")); @@ -421,7 +516,7 @@ public void shouldAcceptCustomArrayClaimOfTypeInteger() throws Exception { } @Test - public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { + public void shouldAcceptCustomArrayClaimOfTypeLong() { String jwt = JWTCreator.init() .withArrayClaim("name", new Long[]{1L, 2L, 3L}) .sign(Algorithm.HMAC256("secret")); @@ -432,7 +527,7 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { } @Test - public void shouldAcceptCustomClaimOfTypeMap() throws Exception { + public void shouldAcceptCustomClaimOfTypeMap() { Map data = new HashMap<>(); data.put("test1", "abc"); data.put("test2", "def"); @@ -446,7 +541,7 @@ public void shouldAcceptCustomClaimOfTypeMap() throws Exception { } @Test - public void shouldRefuseCustomClaimOfTypeUserPojo() throws Exception { + public void shouldRefuseCustomClaimOfTypeUserPojo() { Map data = new HashMap<>(); data.put("test1", new UserPojo("Michael", 255)); @@ -467,7 +562,8 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { data.put("integer", 1); data.put("long", Long.MAX_VALUE); data.put("double", 123.456d); - data.put("date", new Date(123L)); + data.put("date", new Date(123000L)); + data.put("instant", Instant.ofEpochSecond(123)); data.put("boolean", true); // array types @@ -489,7 +585,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"); @@ -497,13 +593,15 @@ public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { 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("instant"), 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"}))); + assertThat(map.get("intArray"), is(Arrays.asList(3, 5))); + assertThat(map.get("longArray"), is(Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE))); + assertThat(map.get("stringArray"), is(Collections.singletonList("string"))); // list assertThat(map.get("list"), is(Arrays.asList("a", "b", "c"))); @@ -521,7 +619,8 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { data.add(1); data.add(Long.MAX_VALUE); data.add(123.456d); - data.add(new Date(123L)); + data.add(new Date(123000L)); + data.add(Instant.ofEpochSecond(123)); data.add(true); // array types @@ -543,7 +642,7 @@ 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"); @@ -552,21 +651,21 @@ public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { 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)); + assertThat(list.get(5), is(123)); + assertThat(list.get(6), 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"}))); + assertThat(list.get(7), is(Arrays.asList(3, 5))); + assertThat(list.get(8), is(Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE))); + assertThat(list.get(9), is(Arrays.asList("string"))); // list - assertThat(list.get(9), is(Arrays.asList("a", "b", "c"))); - assertThat(list.get(10), is(sub)); - + assertThat(list.get(10), is(Arrays.asList("a", "b", "c"))); + assertThat(list.get(11), is(sub)); } @Test - public void shouldAcceptCustomClaimForNullListItem() throws Exception { + public void shouldAcceptCustomClaimForNullListItem() { Map data = new HashMap<>(); data.put("test1", Arrays.asList("a", null, "c")); @@ -576,111 +675,386 @@ public void shouldAcceptCustomClaimForNullListItem() throws Exception { } @Test - @SuppressWarnings("unchecked") - public void shouldAcceptCustomClaimWithNullMapAndRemoveClaim() throws Exception { - String jwt = JWTCreator.init() - .withClaim("map", "stubValue") - .withClaim("map", (Map) null) - .sign(Algorithm.HMAC256("secret")); + public void shouldRefuseCustomClaimForNullMapKey() { + Map data = new HashMap<>(); + data.put(null, "subValue"); - assertThat(jwt, is(notNullValue())); - String[] parts = jwt.split("\\."); + exception.expect(IllegalArgumentException.class); - 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()); + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); } + @SuppressWarnings({"unchecked", "rawtypes"}) @Test - @SuppressWarnings("unchecked") - public void shouldAcceptCustomClaimWithNullListAndRemoveClaim() throws Exception { - String jwt = JWTCreator.init() - .withClaim("list", "stubValue") - .withClaim("list", (List) null) - .sign(Algorithm.HMAC256("secret")); + public void shouldRefuseCustomMapClaimForNonStringKey() { + Map data = new HashMap<>(); + data.put(new Object(), "value"); - assertThat(jwt, is(notNullValue())); - String[] parts = jwt.split("\\."); + exception.expect(IllegalArgumentException.class); - 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()); + JWTCreator.init() + .withClaim("pojo", (Map) data) + .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomClaimForNullMapValue() throws Exception { - Map data = new HashMap<>(); - data.put("subKey", null); + public void shouldRefuseCustomListClaimForUnknownListElement() { + List list = Collections.singletonList(new UserPojo("Michael", 255)); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", data) + .withClaim("list", list) .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomClaimForNullMapKey() throws Exception { + public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() { + List list = Collections.singletonList(new UserPojo("Michael", 255)); + Map data = new HashMap<>(); - data.put(null, "subValue"); + data.put("someList", list); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", data) + .withClaim("list", list) .sign(Algorithm.HMAC256("secret")); } - @SuppressWarnings({"unchecked", "rawtypes"}) @Test - public void shouldRefuseCustomMapClaimForNonStringKey() throws Exception { - Map data = new HashMap<>(); - data.put(new Object(), "value"); + public void shouldRefuseCustomListClaimForUnknownArrayType() { + List list = new ArrayList<>(); + list.add(new Object[]{"test"}); exception.expect(IllegalArgumentException.class); JWTCreator.init() - .withClaim("pojo", (Map) data) + .withClaim("list", list) .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownListElement() throws Exception { - List list = Arrays.asList(new UserPojo("Michael", 255)); + 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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry("asd", 123)); + } + + @Test + public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() { + Map nullMap = null; + String nullString = null; + String jwt = JWTCreator.init() + .withPayload(nullMap) + .withPayload(nullString) + .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, is("{}")); + } + + @Test + public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKey() { + Map payload = new HashMap<>(); + payload.put(HeaderParams.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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); + } + + @Test + public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() { + Map payload = new HashMap<>(); + payload.put(RegisteredClaims.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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + assertThat(payloadJson, JsonMatcher.hasEntry(RegisteredClaims.ISSUER, "abc")); + } + + @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, Instant, and Null"); + Map payload = new HashMap<>(); + payload.put("entry", "value"); + payload.put("pojo", new UserPojo("name", 42)); JWTCreator.init() - .withClaim("list", list) + .withPayload(payload) .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() throws Exception { - List list = Arrays.asList(new UserPojo("Michael", 255)); + 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")); - Map data = new HashMap<>(); - data.put("someList", list); + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.getUrlDecoder().decode(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, Date, Instant, and Null"); + Map payload = new HashMap<>(); + payload.put("list", Arrays.asList("item1", new UserPojo("name", 42))); JWTCreator.init() - .withClaim("list", list) + .withPayload(payload) .sign(Algorithm.HMAC256("secret")); } @Test - public void shouldRefuseCustomListClaimForUnknownArrayType() throws Exception { - List list = new ArrayList<>(); - list.add(new Object[]{"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, Instant, and Null"); + + Map payload = new HashMap<>(); + payload.put("entry", "value"); + payload.put("map", Collections.singletonMap("pojo", new UserPojo("name", 42))); + 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.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)); + 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)); + } + + @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() - .withClaim("list", list) + .withPayload(invalidJson) + .sign(Algorithm.HMAC256("secret")); + } + + @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()); } -} \ No newline at end of file + @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))); + } +} diff --git a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java index dfe59182..cc427d60 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -1,20 +1,20 @@ 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.apache.commons.codec.binary.Base64; import org.hamcrest.collection.IsCollectionWithSize; -import org.hamcrest.core.IsCollectionContaining; -import org.junit.Assert; +import org.hamcrest.core.IsIterableContaining; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.io.*; import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; import java.util.Date; +import java.util.List; import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; @@ -25,7 +25,7 @@ public class JWTDecoderTest { public ExpectedException exception = ExpectedException.none(); @Test - public void getSubject() throws Exception { + public void getSubject() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); assertThat(jwt.getSubject(), is(notNullValue())); assertThat(jwt.getSubject(), is("1234567890")); @@ -33,21 +33,29 @@ public void getSubject() throws Exception { // Exceptions @Test - public void shouldThrowIfLessThan3Parts() throws Exception { + public void shouldThrowIfTheContentIsNotProperlyEncoded() { + exception.expect(JWTDecodeException.class); + exception.expectMessage(startsWith("The string '")); + exception.expectMessage(endsWith("' doesn't have a valid JSON format.")); + JWT.decode("eyJ0eXAiOiJKV1QiLCJhbGciO-corrupted.eyJ0ZXN0IjoxMjN9.sLtFC2rLAzN0-UJ13OLQX6ezNptAQzespaOGwCnpqk"); + } + + @Test + public void shouldThrowIfLessThan3Parts() { exception.expect(JWTDecodeException.class); exception.expectMessage("The token was expected to have 3 parts, but got 2."); JWT.decode("two.parts"); } @Test - public void shouldThrowIfMoreThan3Parts() throws Exception { + 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"); } @Test - public void shouldThrowIfPayloadHasInvalidJSONFormat() throws Exception { + public void shouldThrowIfPayloadHasInvalidJSONFormat() { String validJson = "{}"; String invalidJson = "}{"; exception.expect(JWTDecodeException.class); @@ -56,7 +64,7 @@ public void shouldThrowIfPayloadHasInvalidJSONFormat() throws Exception { } @Test - public void shouldThrowIfHeaderHasInvalidJSONFormat() throws Exception { + public void shouldThrowIfHeaderHasInvalidJSONFormat() { String validJson = "{}"; String invalidJson = "}{"; exception.expect(JWTDecodeException.class); @@ -64,10 +72,28 @@ 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 - public void shouldGetStringToken() throws Exception { + public void shouldGetStringToken() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getToken(), is(notNullValue())); @@ -75,131 +101,126 @@ public void shouldGetStringToken() throws Exception { } @Test - public void shouldGetHeader() throws Exception { + public void shouldGetHeader() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getHeader(), is("eyJhbGciOiJIUzI1NiJ9")); } @Test - public void shouldGetPayload() throws Exception { + public void shouldGetPayload() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getPayload(), is("e30")); } @Test - public void shouldGetSignature() throws Exception { + public void shouldGetSignature() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getSignature(), is("XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); } - // Public PublicClaims + // Standard Claims @Test - public void shouldGetIssuer() throws Exception { + public void shouldGetIssuer() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getIssuer(), is("John Doe")); } @Test - public void shouldGetSubject() throws Exception { + public void shouldGetSubject() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getSubject(), is("Tok3ns")); } @Test - public void shouldGetArrayAudience() throws Exception { + public void shouldGetArrayAudience() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getAudience(), is(IsCollectionWithSize.hasSize(3))); - assertThat(jwt.getAudience(), is(IsCollectionContaining.hasItems("Hope", "Travis", "Solomon"))); + assertThat(jwt.getAudience(), is(IsIterableContaining.hasItems("Hope", "Travis", "Solomon"))); } @Test - public void shouldGetStringAudience() throws Exception { + public void shouldGetStringAudience() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getAudience(), is(IsCollectionWithSize.hasSize(1))); - assertThat(jwt.getAudience(), is(IsCollectionContaining.hasItems("Jack Reyes"))); + assertThat(jwt.getAudience(), is(IsIterableContaining.hasItems("Jack Reyes"))); } @Test - public void shouldGetExpirationTime() throws Exception { + public void shouldGetExpirationTime() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NzY3MjcwODZ9.L9dcPHEDQew2u9MkDCORFkfDGcSOsgoPqNY-LUMLEHg"); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getExpiresAt(), is(instanceOf(Date.class))); long ms = 1476727086L * 1000; - Date expectedDate = new Date(ms); - assertThat(jwt.getExpiresAt(), is(notNullValue())); - assertThat(jwt.getExpiresAt(), is(equalTo(expectedDate))); + assertThat(jwt.getExpiresAt(), is(equalTo(new Date(ms)))); + assertThat(jwt.getExpiresAtAsInstant(), is(equalTo(Instant.ofEpochMilli(ms)))); } @Test - public void shouldGetNotBefore() throws Exception { + public void shouldGetNotBefore() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0NzY3MjcwODZ9.tkpD3iCPQPVqjnjpDVp2bJMBAgpVCG9ZjlBuMitass0"); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getNotBefore(), is(instanceOf(Date.class))); long ms = 1476727086L * 1000; - Date expectedDate = new Date(ms); - assertThat(jwt.getNotBefore(), is(notNullValue())); - assertThat(jwt.getNotBefore(), is(equalTo(expectedDate))); + assertThat(jwt.getNotBefore(), is(equalTo(new Date(ms)))); + assertThat(jwt.getNotBeforeAsInstant(), is(equalTo(Instant.ofEpochMilli(ms)))); } @Test - public void shouldGetIssuedAt() throws Exception { + public void shouldGetIssuedAt() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzY3MjcwODZ9.KPjGoW665E8V5_27Jugab8qSTxLk2cgquhPCBfAP0_w"); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getIssuedAt(), is(instanceOf(Date.class))); long ms = 1476727086L * 1000; - Date expectedDate = new Date(ms); - assertThat(jwt.getIssuedAt(), is(notNullValue())); - assertThat(jwt.getIssuedAt(), is(equalTo(expectedDate))); + assertThat(jwt.getIssuedAt(), is(equalTo(new Date(ms)))); + assertThat(jwt.getIssuedAtAsInstant(), is(equalTo(Instant.ofEpochMilli(ms)))); } @Test - public void shouldGetId() throws Exception { + public void shouldGetId() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTY3ODkwIn0.m3zgEfVUFOd-CvL3xG5BuOWLzb0zMQZCqiVNQQOPOvA"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getId(), is("1234567890")); } @Test - public void shouldGetContentType() throws Exception { + public void shouldGetContentType() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsImN0eSI6ImF3ZXNvbWUifQ.e30.AIm-pJDOaAyct9qKMlN-lQieqNDqc3d4erqUZc5SHAs"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getContentType(), is("awesome")); } @Test - public void shouldGetType() throws Exception { + public void shouldGetType() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.e30.WdFmrzx8b9v_a-r6EHC2PTAaWywgm_8LiP8RBRhYwkI"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getType(), is("JWS")); } @Test - public void shouldGetAlgorithm() throws Exception { + public void shouldGetAlgorithm() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getAlgorithm(), is("HS256")); } - //Private PublicClaims + // Private Claims @Test - public void shouldGetMissingClaimIfClaimDoesNotExist() throws Exception { + 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 - public void shouldGetValidClaim() throws Exception { + public void shouldGetValidClaim() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifX0.lrU1gZlOdlmTTeZwq0VI-pZx2iV46UWYd5-lCjy6-c4"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaim("object"), is(notNullValue())); @@ -207,7 +228,7 @@ public void shouldGetValidClaim() throws Exception { } @Test - public void shouldNotGetNullClaimIfClaimIsEmptyObject() throws Exception { + public void shouldNotGetNullClaimIfClaimIsEmptyObject() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnt9fQ.d3nUeeL_69QsrHL0ZWij612LHEQxD8EZg1rNoY3a4aI"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaim("object"), is(notNullValue())); @@ -215,67 +236,91 @@ public void shouldNotGetNullClaimIfClaimIsEmptyObject() throws Exception { } @Test - public void shouldGetCustomClaimOfTypeInteger() throws Exception { + public void shouldGetCustomClaimOfTypeInteger() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMjN9.XZAudnA7h3_Al5kJydzLjw6RzZC3Q6OvnLEYlhNW7HA"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asInt(), is(123)); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asInt(), is(123)); } @Test - public void shouldGetCustomClaimOfTypeDouble() throws Exception { + public void shouldGetCustomClaimOfTypeDouble() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoyMy40NX0.7pyX2OmEGaU9q15T8bGFqRm-d3RVTYnqmZNZtxMKSlA"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asDouble(), is(23.45)); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asDouble(), is(23.45)); } @Test - public void shouldGetCustomClaimOfTypeBoolean() throws Exception { + public void shouldGetCustomClaimOfTypeBoolean() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjp0cnVlfQ.FwQ8VfsZNRqBa9PXMinSIQplfLU4-rkCLfIlTLg_MV0"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asBoolean(), is(true)); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asBoolean(), is(true)); } @Test - public void shouldGetCustomClaimOfTypeDate() throws Exception { + public void shouldGetCustomClaimOfTypeDate() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; Date date = new Date(1478891521000L); DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asDate().getTime(), is(date.getTime())); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asDate().getTime(), is(date.getTime())); + } + + @Test + public void shouldGetCustomClaimOfTypeInstant() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; + Instant instant = Instant.ofEpochSecond(1478891521L); + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asInstant(), is(equalTo(instant))); } @Test - public void shouldGetCustomArrayClaimOfTypeString() throws Exception { + public void shouldGetCustomArrayClaimOfTypeString() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbInRleHQiLCIxMjMiLCJ0cnVlIl19.lxM8EcmK1uSZRAPd0HUhXGZJdauRmZmLjoeqz4J9yAA"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asArray(String.class), arrayContaining("text", "123", "true")); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asArray(String.class), arrayContaining("text", "123", "true")); } @Test - public void shouldGetCustomArrayClaimOfTypeInteger() throws Exception { + public void shouldGetCustomArrayClaimOfTypeInteger() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); - Assert.assertThat(jwt.getClaim("name").asArray(Integer.class), arrayContaining(1, 2, 3)); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asArray(Integer.class), arrayContaining(1, 2, 3)); } @Test - public void shouldGetCustomMapClaim() throws Exception { - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp7InN0cmluZyI6InZhbHVlIiwibnVtYmVyIjoxLCJib29sZWFuIjp0cnVlfX0.-8aIaXd2-rp1lLuDEQmCeisCBX9X_zbqdPn2llGxNoc"; + public void shouldGetCustomMapClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp7InN0cmluZyI6InZhbHVlIiwibnVtYmVyIjoxLCJib29sZWFuIjp0cnVlLCJlbXB0eSI6bnVsbH19.6xkCuYZnu4RA0xZSxlYSYAqzy9JDWsDtIWqSCUZlPt8"; DecodedJWT jwt = JWT.decode(token); - Assert.assertThat(jwt, is(notNullValue())); + assertThat(jwt, is(notNullValue())); Map map = jwt.getClaim("name").asMap(); - Assert.assertThat(map, hasEntry("string", (Object) "value")); - Assert.assertThat(map, hasEntry("number", (Object) 1)); - Assert.assertThat(map, hasEntry("boolean", (Object) true)); + assertThat(map, hasEntry("string", "value")); + assertThat(map, hasEntry("number", 1)); + assertThat(map, hasEntry("boolean", true)); + assertThat(map, hasEntry("empty", null)); } @Test - public void shouldGetAvailableClaims() throws Exception { + 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 + public void shouldGetAvailableClaims() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwibmJmIjoxMjM0NTY3ODkwLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.2_0nxDPJwOk64U5V5V9pt8U92jTPJbGsHYQ35HYhbdE"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaims(), is(notNullValue())); @@ -318,11 +363,40 @@ public void shouldSerializeAndDeserialize() throws Exception { is(equalTo(deserializedJwt.getClaims().get("extraClaim").asString()))); } + @Test + public void shouldDecodeHeaderClaims() { + String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImRhdGUiOjE2NDczNTgzMjUsInN0cmluZyI6InN0cmluZyIsImJvb2wiOnRydWUsImRvdWJsZSI6MTIzLjEyMywibGlzdCI6WzE2NDczNTgzMjVdLCJtYXAiOnsiZGF0ZSI6MTY0NzM1ODMyNSwiaW5zdGFudCI6MTY0NzM1ODMyNX0sImludCI6NDIsImxvbmciOjQyMDAwMDAwMDAsImluc3RhbnQiOjE2NDczNTgzMjV9.eyJpYXQiOjE2NDczNjA4ODF9.S2nZDM03ZDvLMeJLWOIqWZ9kmYHZUueyQiIZCCjYNL8"; + + Instant expectedInstant = Instant.ofEpochSecond(1647358325); + Date expectedDate = Date.from(expectedInstant); + + DecodedJWT decoded = JWT.decode(jwt); + assertThat(decoded, is(notNullValue())); + assertThat(decoded.getHeaderClaim("date").asDate(), is(expectedDate)); + assertThat(decoded.getHeaderClaim("instant").asInstant(), is(expectedInstant)); + assertThat(decoded.getHeaderClaim("string").asString(), is("string")); + assertThat(decoded.getHeaderClaim("bool").asBoolean(), is(true)); + assertThat(decoded.getHeaderClaim("double").asDouble(), is(123.123)); + assertThat(decoded.getHeaderClaim("int").asInt(), is(42)); + assertThat(decoded.getHeaderClaim("long").asLong(), is(4200000000L)); + + Map headerMap = decoded.getHeaderClaim("map").asMap(); + assertThat(headerMap, is(notNullValue())); + assertThat(headerMap.size(), is(2)); + assertThat(headerMap, hasEntry("date", 1647358325)); + assertThat(headerMap, hasEntry("instant", 1647358325)); + + List headerList = decoded.getHeaderClaim("list").asList(Object.class); + assertThat(headerList, is(notNullValue())); + assertThat(headerList.size(), is(1)); + assertThat(headerList, contains(1647358325)); + } + //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 7cbe4b22..087f1e9e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -1,11 +1,9 @@ package com.auth0.jwt; 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.hamcrest.core.IsIterableContaining; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -13,12 +11,15 @@ import java.nio.charset.StandardCharsets; 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; import java.util.Date; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class JWTTest { @@ -32,14 +33,13 @@ public class JWTTest { 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"; - @Rule public ExpectedException exception = ExpectedException.none(); // Decode @Test - public void shouldDecodeAStringToken() throws Exception { + public void shouldDecodeAStringToken() { String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; DecodedJWT jwt = JWT.decode(token); @@ -47,7 +47,7 @@ public void shouldDecodeAStringToken() throws Exception { } @Test - public void shouldDecodeAStringTokenUsingInstance() throws Exception { + public void shouldDecodeAStringTokenUsingInstance() { String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; JWT jwt = new JWT(); DecodedJWT decodedJWT = jwt.decodeJwt(token); @@ -57,7 +57,7 @@ public void shouldDecodeAStringTokenUsingInstance() throws Exception { // getToken @Test - public void shouldGetStringToken() throws Exception { + public void shouldGetStringToken() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getToken(), is(notNullValue())); @@ -66,7 +66,7 @@ public void shouldGetStringToken() throws Exception { // getToken @Test - public void shouldGetStringTokenUsingInstance() throws Exception { + public void shouldGetStringTokenUsingInstance() { JWT jwt = new JWT(); DecodedJWT decodedJWT = jwt.decodeJwt("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(decodedJWT, is(notNullValue())); @@ -82,14 +82,14 @@ 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())); } @Test - public void shouldAcceptNoneAlgorithm() throws Exception { + public void shouldAcceptNoneAlgorithm() { String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9."; DecodedJWT jwt = JWT.require(Algorithm.none()) .build() @@ -99,7 +99,7 @@ public void shouldAcceptNoneAlgorithm() throws Exception { } @Test - public void shouldAcceptHMAC256Algorithm() throws Exception { + public void shouldAcceptHMAC256Algorithm() { String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -109,7 +109,7 @@ public void shouldAcceptHMAC256Algorithm() throws Exception { } @Test - public void shouldAcceptHMAC384Algorithm() throws Exception { + public void shouldAcceptHMAC384Algorithm() { String token = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; DecodedJWT jwt = JWT.require(Algorithm.HMAC384("secret")) .build() @@ -119,7 +119,7 @@ public void shouldAcceptHMAC384Algorithm() throws Exception { } @Test - public void shouldAcceptHMAC512Algorithm() throws Exception { + public void shouldAcceptHMAC512Algorithm() { String token = "eyJhbGciOiJIUzUxMiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.VUo2Z9SWDV-XcOc_Hr6Lff3vl7L9e5Vb8ThXpmGDFjHxe3Dr1ZBmUChYF-xVA7cAdX1P_D4ZCUcsv3IefpVaJw"; DecodedJWT jwt = JWT.require(Algorithm.HMAC512("secret")) .build() @@ -198,10 +198,10 @@ public void shouldAcceptECDSA512Algorithm() throws Exception { } - // Public Claims + // Standard Claims @Test - public void shouldGetAlgorithm() throws Exception { + public void shouldGetAlgorithm() { String token = "eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -212,7 +212,7 @@ public void shouldGetAlgorithm() throws Exception { } @Test - public void shouldGetSignature() throws Exception { + public void shouldGetSignature() { String token = "eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -223,7 +223,7 @@ public void shouldGetSignature() throws Exception { } @Test - public void shouldGetIssuer() throws Exception { + public void shouldGetIssuer() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -234,7 +234,7 @@ public void shouldGetIssuer() throws Exception { } @Test - public void shouldGetSubject() throws Exception { + public void shouldGetSubject() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -245,7 +245,7 @@ public void shouldGetSubject() throws Exception { } @Test - public void shouldGetArrayAudience() throws Exception { + public void shouldGetArrayAudience() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -253,11 +253,11 @@ public void shouldGetArrayAudience() throws Exception { assertThat(jwt, is(notNullValue())); assertThat(jwt.getAudience(), is(IsCollectionWithSize.hasSize(3))); - assertThat(jwt.getAudience(), is(IsCollectionContaining.hasItems("Hope", "Travis", "Solomon"))); + assertThat(jwt.getAudience(), is(IsIterableContaining.hasItems("Hope", "Travis", "Solomon"))); } @Test - public void shouldGetStringAudience() throws Exception { + public void shouldGetStringAudience() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -265,32 +265,30 @@ public void shouldGetStringAudience() throws Exception { assertThat(jwt, is(notNullValue())); assertThat(jwt.getAudience(), is(IsCollectionWithSize.hasSize(1))); - assertThat(jwt.getAudience(), is(IsCollectionContaining.hasItems("Jack Reyes"))); + assertThat(jwt.getAudience(), is(IsIterableContaining.hasItems("Jack Reyes"))); } @Test - public void shouldGetExpirationTime() throws Exception { - Date expectedDate = new Date(1477592 * 1000); - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(expectedDate); + public void shouldGetExpirationTime() { + long seconds = 1477592L; + 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())); - assertThat(jwt.getExpiresAt(), is(instanceOf(Date.class))); - assertThat(jwt.getExpiresAt(), is(notNullValue())); - assertThat(jwt.getExpiresAt(), is(equalTo(expectedDate))); + assertThat(jwt.getExpiresAt(), is(equalTo(new Date(seconds * 1000)))); + assertThat(jwt.getExpiresAtAsInstant(), is(equalTo(Instant.ofEpochSecond(seconds)))); + } @Test - public void shouldGetNotBefore() throws Exception { - Date expectedDate = new Date(1477592 * 1000); - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(expectedDate); + public void shouldGetNotBefore() { + long seconds = 1477592; + Clock clock = Clock.fixed(Instant.ofEpochSecond(seconds), ZoneId.of("UTC")); String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTJ9.mWYSOPoNXstjKbZkKrqgkwPOQWEx3F3gMm6PMcfuJd8"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); @@ -299,16 +297,14 @@ public void shouldGetNotBefore() throws Exception { .verify(token); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getNotBefore(), is(instanceOf(Date.class))); - assertThat(jwt.getNotBefore(), is(notNullValue())); - assertThat(jwt.getNotBefore(), is(equalTo(expectedDate))); + assertThat(jwt.getNotBefore(), is(equalTo(new Date(seconds * 1000)))); + assertThat(jwt.getNotBeforeAsInstant(), is(equalTo(Instant.ofEpochSecond(seconds)))); } @Test - public void shouldGetIssuedAt() throws Exception { - Date expectedDate = new Date(1477592 * 1000); - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(expectedDate); + public void shouldGetIssuedAt() { + long seconds = 1477592; + Clock clock = Clock.fixed(Instant.ofEpochSecond(seconds), ZoneId.of("UTC")); String token = "eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0Nzc1OTJ9.5o1CKlLFjKKcddZzoarQ37pq7qZqNPav3sdZ_bsZaD4"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); @@ -317,13 +313,12 @@ public void shouldGetIssuedAt() throws Exception { .verify(token); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getIssuedAt(), is(instanceOf(Date.class))); - assertThat(jwt.getIssuedAt(), is(notNullValue())); - assertThat(jwt.getIssuedAt(), is(equalTo(expectedDate))); + assertThat(jwt.getIssuedAt(), is(equalTo(new Date(seconds * 1000)))); + assertThat(jwt.getIssuedAtAsInstant(), is(equalTo(Instant.ofEpochSecond(seconds)))); } @Test - public void shouldGetId() throws Exception { + public void shouldGetId() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTY3ODkwIn0.m3zgEfVUFOd-CvL3xG5BuOWLzb0zMQZCqiVNQQOPOvA"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification @@ -335,7 +330,7 @@ public void shouldGetId() throws Exception { } @Test - public void shouldGetContentType() throws Exception { + public void shouldGetContentType() { String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6ImF3ZXNvbWUifQ.e30.AIm-pJDOaAyct9qKMlN-lQieqNDqc3d4erqUZc5SHAs"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -346,7 +341,7 @@ public void shouldGetContentType() throws Exception { } @Test - public void shouldGetType() throws Exception { + public void shouldGetType() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.e30.WdFmrzx8b9v_a-r6EHC2PTAaWywgm_8LiP8RBRhYwkI"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -357,7 +352,7 @@ public void shouldGetType() throws Exception { } @Test - public void shouldGetKeyId() throws Exception { + public void shouldGetKeyId() { String token = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImtleSJ9.e30.von1Vt9tq9cn5ZYdX1f4cf2EE7fUvb5BCBlKOTm9YWs"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -368,7 +363,7 @@ public void shouldGetKeyId() throws Exception { } @Test - public void shouldGetCustomClaims() throws Exception { + public void shouldGetCustomClaims() { String token = "eyJhbGciOiJIUzI1NiIsImlzQWRtaW4iOnRydWV9.eyJpc0FkbWluIjoibm9wZSJ9.YDKBAgUDbh0PkhioDcLNzdQ8c2Gdf_yS6zdEtJQS3F0"; DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) .build() @@ -384,12 +379,12 @@ public void shouldGetCustomClaims() throws Exception { // Sign @Test - public void shouldCreateAnEmptyHMAC256SignedToken() throws Exception { + public void shouldCreateAnEmptyHMAC256SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC256("secret")); 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")); @@ -400,12 +395,12 @@ public void shouldCreateAnEmptyHMAC256SignedToken() throws Exception { } @Test - public void shouldCreateAnEmptyHMAC384SignedToken() throws Exception { + public void shouldCreateAnEmptyHMAC384SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC384("secret")); 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")); @@ -416,12 +411,12 @@ public void shouldCreateAnEmptyHMAC384SignedToken() throws Exception { } @Test - public void shouldCreateAnEmptyHMAC512SignedToken() throws Exception { + public void shouldCreateAnEmptyHMAC512SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC512("secret")); 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")); @@ -437,7 +432,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")); @@ -453,7 +448,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")); @@ -469,7 +464,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")); @@ -485,7 +480,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")); @@ -501,7 +496,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")); @@ -517,7 +512,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/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index aec70c70..732d6365 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -1,55 +1,71 @@ package com.auth0.jwt; import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.AlgorithmMismatchException; -import com.auth0.jwt.exceptions.InvalidClaimException; -import com.auth0.jwt.exceptions.TokenExpiredException; -import com.auth0.jwt.interfaces.Clock; +import com.auth0.jwt.exceptions.*; +import com.auth0.jwt.interfaces.Claim; 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; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +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.junit.Assert.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class JWTVerifierTest { - private static final long DATE_TOKEN_MS_VALUE = 1477592 * 1000; + private final Clock mockNow = Clock.fixed(Instant.ofEpochSecond(1477592), ZoneId.of("UTC")); + private final Clock mockOneSecondEarlier = Clock.offset(mockNow, Duration.ofSeconds(-1)); + private final Clock mockOneSecondLater = Clock.offset(mockNow, Duration.ofSeconds(1)); + @Rule public ExpectedException exception = ExpectedException.none(); @Test - public void shouldThrowWhenInitializedWithoutAlgorithm() throws Exception { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("The Algorithm cannot be null"); - JWTVerifier.init(null); + public void shouldThrowWhenInitializedWithoutAlgorithm() { + IllegalArgumentException e = assertThrows(null, IllegalArgumentException.class, () -> + JWTVerifier.init(null)); + assertThat(e.getMessage(), is("The Algorithm cannot be null.")); } @Test - public void shouldThrowWhenAlgorithmDoesntMatchTheTokensAlgorithm() throws Exception { - 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"); + public void shouldThrowWhenAlgorithmDoesntMatchTheTokensAlgorithm() { + 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 - public void shouldValidateIssuer() throws Exception { + public void shouldValidateIssuer() { String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .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 @@ -65,30 +81,50 @@ public void shouldValidateMultipleIssuers() { } @Test - public void shouldThrowOnInvalidIssuer() throws Exception { - exception.expect(InvalidClaimException.class); - 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") - .build() - .verify(token); + public void shouldThrowOnInvalidIssuer() { + 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(RegisteredClaims.ISSUER)); + assertThat(e.getClaimValue().asString(), is("auth0")); } @Test - public void shouldThrowOnNullIssuer() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'iss' value doesn't match the required issuer."); + public void shouldThrowOnNullIssuer() { + 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(RegisteredClaims.ISSUER)); + assertThat(e.getClaimValue().isNull(), is(true)); + } - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withIssuer("auth0") - .build() - .verify(token); + @Test + public void shouldThrowOnMissingIssuer() { + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String jwt = JWTCreator.init() + .sign(Algorithm.HMAC256("secret")); + + 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 - public void shouldValidateSubject() throws Exception { + public void shouldValidateSubject() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withSubject("1234567890") @@ -99,18 +135,22 @@ public void shouldValidateSubject() throws Exception { } @Test - public void shouldThrowOnInvalidSubject() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'sub' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withSubject("invalid") - .build() - .verify(token); + public void shouldThrowOnInvalidSubject() { + 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(RegisteredClaims.SUBJECT)); + assertThat(e.getClaimValue().asString(), is("1234567890")); } @Test - public void shouldValidateAudience() throws Exception { + public void shouldAcceptAudienceWhenWithAudienceContainsAll() { + // Token 'aud': ["Mark"] String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJNYXJrIn0.xWB6czYI0XObbVhLAxe55TwChWZg7zO08RxONWU2iY4"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience("Mark") @@ -119,6 +159,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") @@ -126,11 +167,55 @@ public void shouldValidateAudience() throws Exception { .verify(tokenArr); assertThat(jwtArr, is(notNullValue())); - } + } + + @Test + public void shouldAllowWithAnyOfAudienceVerificationToOverrideWithAudience() { + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAudience("Mark", "Jim"); + + Exception exception = null; + try { + verification.build().verify(token); + } catch (Exception e) { + exception = e; + + } + + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(IncorrectClaimException.class))); + assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")).withAnyOfAudience("Mark", "Jim").build().verify(token); + assertThat(jwt, is(notNullValue())); + } @Test - public void shouldAcceptPartialAudience() throws Exception { - //Token 'aud' = ["Mark", "David", "John"] + public void shouldAllowWithAudienceVerificationToOverrideWithAnyOfAudience() { + // Token 'aud' = ["Mark", "David", "John"] + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withAnyOfAudience("Jim"); + + Exception exception = null; + try { + verification.build().verify(token); + } catch (Exception e) { + exception = e; + + } + + assertThat(exception, is(notNullValue())); + assertThat(exception, is(instanceOf(IncorrectClaimException.class))); + assertThat(exception.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")).withAudience("Mark").build().verify(token); + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldAcceptAudienceWhenWithAudienceAndPartialExpected() { + // Token 'aud' = ["Mark", "David", "John"] String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience("John") @@ -141,71 +226,119 @@ public void shouldAcceptPartialAudience() throws Exception { } @Test - public void shouldThrowOnInvalidAudience() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'aud' value doesn't contain the required audience."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withAudience("nope") + public void shouldAcceptAudienceWhenAnyOfAudienceAndAllContained() { + // Token 'aud' = ["Mark", "David", "John"] + String tokenArr = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiTWFyayIsIkRhdmlkIiwiSm9obiJdfQ.DX5xXiCaYvr54x_iL0LZsJhK7O6HhAdHeDYkgDeb0Rw"; + DecodedJWT jwtArr = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience("Mark", "David", "John") .build() - .verify(token); + .verify(tokenArr); + + assertThat(jwtArr, is(notNullValue())); } @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(); + public void shouldThrowWhenAudienceHasNoneOfExpectedAnyOfAudience() { + 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(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); + } - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + @Test + public void shouldThrowWhenAudienceClaimDoesNotContainAllExpected() { + 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(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"Mark","David","John"})); + } - String emptyAud = " "; - verifier = JWTVerifier.init(algorithm) - .withAudience(emptyAud) - .build(); + @Test + public void shouldThrowWhenAudienceClaimIsNull() { + 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(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().isNull(), is(true)); + } - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, hasEntry("aud", Collections.singletonList(emptyAud))); + @Test + public void shouldThrowWhenAudienceClaimIsMissing(){ + 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 shouldRemoveAudienceWhenPassingNull() throws Exception { - Algorithm algorithm = mock(Algorithm.class); - JWTVerifier verifier = JWTVerifier.init(algorithm) - .withAudience("John") - .withAudience((String) null) - .build(); + public void shouldThrowWhenAudienceClaimIsNullWithAnAudience() { + 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(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {null})); + } - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + @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")); + } - verifier = JWTVerifier.init(algorithm) - .withAudience("John") + @Test + public void shouldNotReplaceWhenMultipleChecksAreAdded() { + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) .withAudience((String[]) null) + .withAudience() + .withAnyOfAudience((String[]) null) + .withAnyOfAudience() .build(); - assertThat(verifier.claims, is(notNullValue())); - assertThat(verifier.claims, not(hasKey("aud"))); + assertThat(verifier.expectedChecks.size(), is(7)); //3 extra mandatory checks exp, nbf, iat } @Test - public void shouldThrowOnNullCustomClaimName() throws Exception { + public void shouldThrowOnNullCustomClaimName() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Custom Claim's name can't be null."); JWTVerifier.init(Algorithm.HMAC256("secret")) @@ -213,96 +346,118 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { } @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); + public void shouldThrowWhenExpectedArrayClaimIsMissing() { + 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() 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); + public void shouldThrowWhenExpectedClaimIsMissing() { + 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() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", "value") - .build() - .verify(token); + public void shouldThrowOnInvalidCustomClaimValueOfTypeString() { + 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() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 123) - .build() - .verify(token); + public void shouldThrowOnInvalidCustomClaimValueOfTypeInteger() { + 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() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 23.45) - .build() - .verify(token); + public void shouldThrowOnInvalidCustomClaimValueOfTypeDouble() { + 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() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", true) - .build() - .verify(token); + public void shouldThrowOnInvalidCustomClaimValueOfTypeBoolean() { + 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() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'name' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", new Date()) - .build() - .verify(token); + public void shouldThrowOnInvalidCustomClaimValueOfTypeDate() { + 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() throws Exception { - 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, new ClockImpl()); - verifier.verify(token); + public void shouldThrowOnInvalidCustomClaimValue() { + 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 - public void shouldValidateCustomClaimOfTypeString() throws Exception { + public void shouldValidateCustomClaimOfTypeString() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidmFsdWUifQ.Jki8pvw6KGbxpMinufrgo6RDL1cu7AtNMJYVh6t-_cE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", "value") @@ -313,7 +468,7 @@ public void shouldValidateCustomClaimOfTypeString() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeInteger() throws Exception { + public void shouldValidateCustomClaimOfTypeInteger() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMjN9.XZAudnA7h3_Al5kJydzLjw6RzZC3Q6OvnLEYlhNW7HA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", 123) @@ -324,7 +479,7 @@ public void shouldValidateCustomClaimOfTypeInteger() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeLong() throws Exception { + public void shouldValidateCustomClaimOfTypeLong() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjo5MjIzMzcyMDM2ODU0Nzc2MDB9.km-IwQ5IDnTZFmuJzhSgvjTzGkn_Z5X29g4nAuVC56I"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", 922337203685477600L) @@ -335,7 +490,7 @@ public void shouldValidateCustomClaimOfTypeLong() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeDouble() throws Exception { + public void shouldValidateCustomClaimOfTypeDouble() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoyMy40NX0.7pyX2OmEGaU9q15T8bGFqRm-d3RVTYnqmZNZtxMKSlA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", 23.45) @@ -346,7 +501,7 @@ public void shouldValidateCustomClaimOfTypeDouble() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeBoolean() throws Exception { + public void shouldValidateCustomClaimOfTypeBoolean() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjp0cnVlfQ.FwQ8VfsZNRqBa9PXMinSIQplfLU4-rkCLfIlTLg_MV0"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", true) @@ -357,9 +512,9 @@ public void shouldValidateCustomClaimOfTypeBoolean() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeDate() throws Exception { + public void shouldValidateCustomClaimOfTypeDate() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; - Date date = new Date(1478891521000L); + Date date = new Date(1478891521123L); DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", date) .build() @@ -369,7 +524,18 @@ public void shouldValidateCustomClaimOfTypeDate() throws Exception { } @Test - public void shouldValidateCustomArrayClaimOfTypeString() throws Exception { + 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.size(), is(5)); + } + + @Test + public void shouldValidateCustomArrayClaimOfTypeString() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbInRleHQiLCIxMjMiLCJ0cnVlIl19.lxM8EcmK1uSZRAPd0HUhXGZJdauRmZmLjoeqz4J9yAA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withArrayClaim("name", "text", "123", "true") @@ -380,7 +546,7 @@ public void shouldValidateCustomArrayClaimOfTypeString() throws Exception { } @Test - public void shouldValidateCustomArrayClaimOfTypeInteger() throws Exception { + public void shouldValidateCustomArrayClaimOfTypeInteger() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withArrayClaim("name", 1, 2, 3) @@ -391,7 +557,7 @@ public void shouldValidateCustomArrayClaimOfTypeInteger() throws Exception { } @Test - public void shouldValidateCustomArrayClaimOfTypeLong() throws Exception { + public void shouldValidateCustomArrayClaimOfTypeLong() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbNTAwMDAwMDAwMDAxLDUwMDAwMDAwMDAwMiw1MDAwMDAwMDAwMDNdfQ.vzV7S0gbV9ZAVxChuIt4XZuSVTxMH536rFmoHzxmayM"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withArrayClaim("name", 500000000001L, 500000000002L, 500000000003L) @@ -402,7 +568,7 @@ public void shouldValidateCustomArrayClaimOfTypeLong() throws Exception { } @Test - public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() throws Exception { + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withArrayClaim("name", 1L, 2L, 3L) @@ -413,7 +579,7 @@ public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() throws } @Test - public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() throws Exception { + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSw1MDAwMDAwMDAwMDIsNTAwMDAwMDAwMDAzXX0.PQjb2rPPpYjM2sItZEzZcjS2YbfPCp6xksTSPjpjTQA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withArrayClaim("name", 1L, 500000000002L, 500000000003L) @@ -424,80 +590,80 @@ public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() } // Generic Delta - @SuppressWarnings("RedundantCast") @Test - public void shouldAddDefaultLeewayToDateClaims() throws Exception { + 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(RegisteredClaims.ISSUED_AT), is(0L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(0L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(0L)); } - @SuppressWarnings("RedundantCast") @Test - public void shouldAddCustomLeewayToDateClaims() throws Exception { + 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(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test - public void shouldOverrideDefaultIssuedAtLeeway() throws Exception { + 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(RegisteredClaims.ISSUED_AT), is(9999L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test - public void shouldOverrideDefaultExpiresAtLeeway() throws Exception { + 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(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(9999L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(1234L)); } - @SuppressWarnings("RedundantCast") @Test - public void shouldOverrideDefaultNotBeforeLeeway() throws Exception { + 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(RegisteredClaims.ISSUED_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.EXPIRES_AT), is(1234L)); + assertThat(verification.getLeewayFor(RegisteredClaims.NOT_BEFORE), is(9999L)); } @Test - public void shouldThrowOnNegativeCustomLeeway() throws Exception { + public void shouldThrowOnNegativeCustomLeeway() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Leeway value can't be negative."); Algorithm algorithm = mock(Algorithm.class); @@ -506,51 +672,59 @@ public void shouldThrowOnNegativeCustomLeeway() throws Exception { } // Expires At - @Test - public void shouldValidateExpiresAtWithLeeway() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE + 1000)); + @Test + public void shouldValidateExpiresAtWithLeeway() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) .acceptExpiresAt(2); DecodedJWT jwt = verification - .build(clock) + .build(mockOneSecondLater) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldValidateExpiresAtIfPresent() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE)); - + public void shouldValidateExpiresAtIfPresent() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification - .build(clock) + .build(mockOneSecondEarlier) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnInvalidExpiresAtIfPresent() throws Exception { - exception.expect(TokenExpiredException.class); - exception.expectMessage(startsWith("The Token has expired on")); - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE + 1000)); + 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))); + } - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(clock) - .verify(token); + @Test + public void shouldThrowOnInvalidExpiresAtIfPresent() { + 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 - public void shouldThrowOnNegativeExpiresAtLeeway() throws Exception { + public void shouldThrowOnNegativeExpiresAtLeeway() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Leeway value can't be negative."); Algorithm algorithm = mock(Algorithm.class); @@ -560,50 +734,55 @@ public void shouldThrowOnNegativeExpiresAtLeeway() throws Exception { // Not before @Test - public void shouldValidateNotBeforeWithLeeway() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); - + public void shouldValidateNotBeforeWithLeeway() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0Nzc1OTJ9.wq4ZmnSF2VOxcQBxPLfeh1J2Ozy1Tj5iUaERm3FKaw8"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) .acceptNotBefore(2); DecodedJWT jwt = verification - .build(clock) + .build(mockOneSecondEarlier) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnInvalidNotBeforeIfPresent() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage(startsWith("The Token can't be used before")); - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + public void shouldThrowOnInvalidNotBeforeIfPresent() { + 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(RegisteredClaims.NOT_BEFORE)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); + } - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0Nzc1OTJ9.wq4ZmnSF2VOxcQBxPLfeh1J2Ozy1Tj5iUaERm3FKaw8"; + @Test + public void shouldValidateNotBeforeIfPresent() { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTN9.f4zVV0TbbTG5xxDjSoGZ320JIMchGoQCWrnT5MyQdT0"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(clock) + DecodedJWT jwt = verification + .build(mockOneSecondLater) .verify(token); + + assertThat(jwt, is(notNullValue())); } @Test - public void shouldValidateNotBeforeIfPresent() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; + public void shouldAcceptNotBeforeEqualToNow() { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTJ9.71XBtRmkAa4iKnyhbS4NPW-Xr26eAVAdHZgmupS7a5o"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification - .build(clock) + .build(mockNow) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnNegativeNotBeforeLeeway() throws Exception { + public void shouldThrowOnNegativeNotBeforeLeeway() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Leeway value can't be negative."); Algorithm algorithm = mock(Algorithm.class); @@ -612,76 +791,71 @@ public void shouldThrowOnNegativeNotBeforeLeeway() 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 = "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())); + @Test + public void shouldThrowOnFutureIssuedAt() { + 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())); + }); + assertThat(e.getMessage(), is("The Token can't be used before 1970-01-18T02:26:32Z.")); + assertThat(e.getClaimName(), is(RegisteredClaims.ISSUED_AT)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); } // 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)); - + public void shouldSkipIssuedAtVerificationWhenFlagIsPassed() { 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); + DecodedJWT jwt = verification.build(mockOneSecondEarlier).verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnInvalidIssuedAtIfPresent() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage(startsWith("The Token can't be used before")); - 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")); - verification - .build(clock) - .verify(token); + public void shouldThrowOnInvalidIssuedAtIfPresent() { + 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(RegisteredClaims.ISSUED_AT)); + assertThat(e.getClaimValue().asLong(), is(1477592L)); } @Test - public void shouldOverrideAcceptIssuedAtWhenIgnoreIssuedAtFlagPassedAndSkipTheVerification() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); - + public void shouldOverrideAcceptIssuedAtWhenIgnoreIssuedAtFlagPassedAndSkipTheVerification() { 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(mockOneSecondEarlier) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldValidateIssuedAtIfPresent() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE)); - + public void shouldValidateIssuedAtIfPresent() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); DecodedJWT jwt = verification - .build(clock) + .build(mockNow) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnNegativeIssuedAtLeeway() throws Exception { + public void shouldThrowOnNegativeIssuedAtLeeway() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Leeway value can't be negative."); Algorithm algorithm = mock(Algorithm.class); @@ -690,7 +864,7 @@ public void shouldThrowOnNegativeIssuedAtLeeway() throws Exception { } @Test - public void shouldValidateJWTId() throws Exception { + public void shouldValidateJWTId() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJqd3RfaWRfMTIzIn0.0kegfXUvwOYioP8PDaLMY1IlV8HOAzSVz3EGL7-jWF4"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withJWTId("jwt_id_123") @@ -701,71 +875,73 @@ public void shouldValidateJWTId() throws Exception { } @Test - public void shouldThrowOnInvalidJWTId() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'jti' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJqd3RfaWRfMTIzIn0.0kegfXUvwOYioP8PDaLMY1IlV8HOAzSVz3EGL7-jWF4"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withJWTId("invalid") - .build() - .verify(token); + public void shouldThrowOnInvalidJWTId() { + 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 - public void shouldRemoveClaimWhenPassingNull() throws Exception { + public void shouldNotRemoveClaimWhenPassingNull() { Algorithm algorithm = mock(Algorithm.class); JWTVerifier 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.size(), is(5)); 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.size(), is(5)); } @Test - public void shouldRemoveIssuerWhenPassingNullReference() throws Exception { + public void shouldNotRemoveIssuerWhenPassingNullReference() { 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"))); + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks.size(), is(4)); 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.size(), is(4)); 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.size(), is(4)); 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 - public void shouldSkipClaimValidationsIfNoClaimsRequired() throws Exception { + public void shouldSkipClaimValidationsIfNoClaimsRequired() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .build() @@ -773,4 +949,369 @@ public void shouldSkipClaimValidationsIfNoClaimsRequired() throws Exception { assertThat(jwt, is(notNullValue())); } + + @Test + public void shouldThrowWhenVerifyingClaimPresenceButClaimNotPresent() { + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("custom", "") + .sign(Algorithm.HMAC256("secret")); + + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaimPresence("missing") + .build(); + + verifier.verify(jwt); + }); + assertThat(e.getMessage(), is("The Claim 'missing' is not present in the JWT.")); + assertThat(e.getClaimName(), is("missing")); + } + + @Test + public void shouldThrowWhenVerifyingClaimPresenceWhenClaimNameIsNull() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Custom Claim's name can't be null."); + + 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())); + } + + @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())) + .build(); + + DecodedJWT decodedJWT = verifier.verify(jwt); + assertThat(decodedJWT, is(notNullValue())); + } + + @Test + public void shouldThrowWhenPredicateReturnsFalse() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + 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); + }); + 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 + 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.size(), is(5)); + } + + @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() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String jwt = JWTCreator.init() + .withClaim("claimName", "value") + .sign(Algorithm.HMAC256("secret")); + + 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() { + 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")); + } + + @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")); + } } diff --git a/lib/src/test/java/com/auth0/jwt/PemUtils.java b/lib/src/test/java/com/auth0/jwt/PemUtils.java index 5f026b0a..6c92e05e 100644 --- a/lib/src/test/java/com/auth0/jwt/PemUtils.java +++ b/lib/src/test/java/com/auth0/jwt/PemUtils.java @@ -43,7 +43,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/TokenUtilsTest.java b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java index 4bea9b44..01806ddc 100644 --- a/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java +++ b/lib/src/test/java/com/auth0/jwt/TokenUtilsTest.java @@ -6,7 +6,7 @@ import org.junit.rules.ExpectedException; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; public class TokenUtilsTest { @@ -14,7 +14,31 @@ public class TokenUtilsTest { public ExpectedException exception = ExpectedException.none(); @Test - public void shouldSplitToken() throws Exception { + 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"; String[] parts = TokenUtils.splitToken(token); @@ -26,7 +50,7 @@ public void shouldSplitToken() throws Exception { } @Test - public void shouldSplitTokenWithEmptySignature() throws Exception { + public void shouldSplitTokenWithEmptySignature() { String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9."; String[] parts = TokenUtils.splitToken(token); @@ -34,22 +58,37 @@ public void shouldSplitTokenWithEmptySignature() throws Exception { 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() throws Exception { + 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() throws Exception { + 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"; 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 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..e09661d3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -12,7 +12,7 @@ import java.security.interfaces.*; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.withSettings; import static org.mockito.ArgumentMatchers.any; @@ -25,7 +25,7 @@ public class AlgorithmTest { @Test - public void shouldThrowHMAC256InstanceWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC256InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -33,7 +33,7 @@ public void shouldThrowHMAC256InstanceWithNullSecretBytes() throws Exception { } @Test - public void shouldThrowHMAC384InstanceWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC384InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -41,7 +41,7 @@ public void shouldThrowHMAC384InstanceWithNullSecretBytes() throws Exception { } @Test - public void shouldThrowHMAC512InstanceWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC512InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -49,7 +49,7 @@ public void shouldThrowHMAC512InstanceWithNullSecretBytes() throws Exception { } @Test - public void shouldThrowHMAC256InstanceWithNullSecret() throws Exception { + public void shouldThrowHMAC256InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -57,7 +57,7 @@ public void shouldThrowHMAC256InstanceWithNullSecret() throws Exception { } @Test - public void shouldThrowHMAC384InstanceWithNullSecret() throws Exception { + public void shouldThrowHMAC384InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -65,7 +65,7 @@ public void shouldThrowHMAC384InstanceWithNullSecret() throws Exception { } @Test - public void shouldThrowHMAC512InstanceWithNullSecret() throws Exception { + public void shouldThrowHMAC512InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -73,7 +73,7 @@ public void shouldThrowHMAC512InstanceWithNullSecret() throws Exception { } @Test - public void shouldThrowRSA256InstanceWithNullKey() throws Exception { + public void shouldThrowRSA256InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); RSAKey key = null; @@ -81,14 +81,14 @@ public void shouldThrowRSA256InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowRSA256InstanceWithNullKeys() throws Exception { + public void shouldThrowRSA256InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.RSA256(null, null); } @Test - public void shouldThrowRSA256InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowRSA256InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); RSAKeyProvider provider = null; @@ -96,7 +96,7 @@ public void shouldThrowRSA256InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldThrowRSA384InstanceWithNullKey() throws Exception { + public void shouldThrowRSA384InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); RSAKey key = null; @@ -104,14 +104,14 @@ public void shouldThrowRSA384InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowRSA384InstanceWithNullKeys() throws Exception { + public void shouldThrowRSA384InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.RSA384(null, null); } @Test - public void shouldThrowRSA384InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowRSA384InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); RSAKeyProvider provider = null; @@ -119,7 +119,7 @@ public void shouldThrowRSA384InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldThrowRSA512InstanceWithNullKey() throws Exception { + public void shouldThrowRSA512InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); RSAKey key = null; @@ -127,14 +127,14 @@ public void shouldThrowRSA512InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowRSA512InstanceWithNullKeys() throws Exception { + public void shouldThrowRSA512InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.RSA512(null, null); } @Test - public void shouldThrowRSA512InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowRSA512InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); RSAKeyProvider provider = null; @@ -142,7 +142,7 @@ public void shouldThrowRSA512InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldThrowECDSA256InstanceWithNullKey() throws Exception { + public void shouldThrowECDSA256InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); ECKey key = null; @@ -150,14 +150,14 @@ public void shouldThrowECDSA256InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowECDSA256InstanceWithNullKeys() throws Exception { + public void shouldThrowECDSA256InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.ECDSA256(null, null); } @Test - public void shouldThrowECDSA256InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowECDSA256InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); ECDSAKeyProvider provider = null; @@ -165,7 +165,7 @@ public void shouldThrowECDSA256InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldThrowECDSA384InstanceWithNullKey() throws Exception { + public void shouldThrowECDSA384InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); ECKey key = null; @@ -173,14 +173,14 @@ public void shouldThrowECDSA384InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowECDSA384InstanceWithNullKeys() throws Exception { + public void shouldThrowECDSA384InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.ECDSA384(null, null); } @Test - public void shouldThrowECDSA384InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowECDSA384InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); ECDSAKeyProvider provider = null; @@ -188,7 +188,7 @@ public void shouldThrowECDSA384InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldThrowECDSA512InstanceWithNullKey() throws Exception { + public void shouldThrowECDSA512InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); ECKey key = null; @@ -196,14 +196,14 @@ public void shouldThrowECDSA512InstanceWithNullKey() throws Exception { } @Test - public void shouldThrowECDSA512InstanceWithNullKeys() throws Exception { + public void shouldThrowECDSA512InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Both provided Keys cannot be null."); Algorithm.ECDSA512(null, null); } @Test - public void shouldThrowECDSA512InstanceWithNullKeyProvider() throws Exception { + public void shouldThrowECDSA512InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Key Provider cannot be null."); ECDSAKeyProvider provider = null; @@ -211,7 +211,7 @@ public void shouldThrowECDSA512InstanceWithNullKeyProvider() throws Exception { } @Test - public void shouldCreateHMAC256AlgorithmWithBytes() throws Exception { + public void shouldCreateHMAC256AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); @@ -221,7 +221,7 @@ public void shouldCreateHMAC256AlgorithmWithBytes() throws Exception { } @Test - public void shouldCreateHMAC384AlgorithmWithBytes() throws Exception { + public void shouldCreateHMAC384AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); @@ -231,7 +231,7 @@ public void shouldCreateHMAC384AlgorithmWithBytes() throws Exception { } @Test - public void shouldCreateHMAC512AlgorithmWithBytes() throws Exception { + public void shouldCreateHMAC512AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); @@ -241,7 +241,7 @@ public void shouldCreateHMAC512AlgorithmWithBytes() throws Exception { } @Test - public void shouldCreateHMAC256AlgorithmWithString() throws Exception { + public void shouldCreateHMAC256AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC256("secret"); assertThat(algorithm, is(notNullValue())); @@ -251,7 +251,7 @@ public void shouldCreateHMAC256AlgorithmWithString() throws Exception { } @Test - public void shouldCreateHMAC384AlgorithmWithString() throws Exception { + public void shouldCreateHMAC384AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC384("secret"); assertThat(algorithm, is(notNullValue())); @@ -261,7 +261,7 @@ public void shouldCreateHMAC384AlgorithmWithString() throws Exception { } @Test - public void shouldCreateHMAC512AlgorithmWithString() throws Exception { + public void shouldCreateHMAC512AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC512("secret"); assertThat(algorithm, is(notNullValue())); @@ -271,7 +271,7 @@ public void shouldCreateHMAC512AlgorithmWithString() throws Exception { } @Test - public void shouldCreateRSA256AlgorithmWithPublicKey() throws Exception { + public void shouldCreateRSA256AlgorithmWithPublicKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA256(key); @@ -282,7 +282,7 @@ public void shouldCreateRSA256AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateRSA256AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateRSA256AlgorithmWithPrivateKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); Algorithm algorithm = Algorithm.RSA256(key); @@ -293,7 +293,7 @@ public void shouldCreateRSA256AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateRSA256AlgorithmWithBothKeys() throws Exception { + public void shouldCreateRSA256AlgorithmWithBothKeys() { RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); @@ -305,7 +305,7 @@ public void shouldCreateRSA256AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateRSA256AlgorithmWithProvider() throws Exception { + public void shouldCreateRSA256AlgorithmWithProvider() { RSAKeyProvider provider = mock(RSAKeyProvider.class); Algorithm algorithm = Algorithm.RSA256(provider); @@ -316,7 +316,7 @@ public void shouldCreateRSA256AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateRSA384AlgorithmWithPublicKey() throws Exception { + public void shouldCreateRSA384AlgorithmWithPublicKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA384(key); @@ -327,7 +327,7 @@ public void shouldCreateRSA384AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateRSA384AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateRSA384AlgorithmWithPrivateKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); Algorithm algorithm = Algorithm.RSA384(key); @@ -338,7 +338,7 @@ public void shouldCreateRSA384AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateRSA384AlgorithmWithBothKeys() throws Exception { + public void shouldCreateRSA384AlgorithmWithBothKeys() { RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); Algorithm algorithm = Algorithm.RSA384(publicKey, privateKey); @@ -350,7 +350,7 @@ public void shouldCreateRSA384AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateRSA384AlgorithmWithProvider() throws Exception { + public void shouldCreateRSA384AlgorithmWithProvider() { RSAKeyProvider provider = mock(RSAKeyProvider.class); Algorithm algorithm = Algorithm.RSA384(provider); @@ -361,7 +361,7 @@ public void shouldCreateRSA384AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateRSA512AlgorithmWithPublicKey() throws Exception { + public void shouldCreateRSA512AlgorithmWithPublicKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA512(key); @@ -372,7 +372,7 @@ public void shouldCreateRSA512AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateRSA512AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateRSA512AlgorithmWithPrivateKey() { RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); Algorithm algorithm = Algorithm.RSA512(key); @@ -383,7 +383,7 @@ public void shouldCreateRSA512AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateRSA512AlgorithmWithBothKeys() throws Exception { + public void shouldCreateRSA512AlgorithmWithBothKeys() { RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); Algorithm algorithm = Algorithm.RSA512(publicKey, privateKey); @@ -395,7 +395,7 @@ public void shouldCreateRSA512AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateRSA512AlgorithmWithProvider() throws Exception { + public void shouldCreateRSA512AlgorithmWithProvider() { RSAKeyProvider provider = mock(RSAKeyProvider.class); Algorithm algorithm = Algorithm.RSA512(provider); @@ -406,7 +406,7 @@ public void shouldCreateRSA512AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateECDSA256AlgorithmWithPublicKey() throws Exception { + public void shouldCreateECDSA256AlgorithmWithPublicKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); Algorithm algorithm = Algorithm.ECDSA256(key); @@ -417,7 +417,7 @@ public void shouldCreateECDSA256AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateECDSA256AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateECDSA256AlgorithmWithPrivateKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); Algorithm algorithm = Algorithm.ECDSA256(key); @@ -428,7 +428,7 @@ public void shouldCreateECDSA256AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateECDSA256AlgorithmWithBothKeys() throws Exception { + public void shouldCreateECDSA256AlgorithmWithBothKeys() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); Algorithm algorithm = Algorithm.ECDSA256(publicKey, privateKey); @@ -440,7 +440,7 @@ public void shouldCreateECDSA256AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateECDSA256AlgorithmWithProvider() throws Exception { + public void shouldCreateECDSA256AlgorithmWithProvider() { ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); Algorithm algorithm = Algorithm.ECDSA256(provider); @@ -451,7 +451,7 @@ public void shouldCreateECDSA256AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateECDSA384AlgorithmWithPublicKey() throws Exception { + public void shouldCreateECDSA384AlgorithmWithPublicKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); Algorithm algorithm = Algorithm.ECDSA384(key); @@ -462,7 +462,7 @@ public void shouldCreateECDSA384AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateECDSA384AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateECDSA384AlgorithmWithPrivateKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); Algorithm algorithm = Algorithm.ECDSA384(key); @@ -473,7 +473,7 @@ public void shouldCreateECDSA384AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateECDSA384AlgorithmWithBothKeys() throws Exception { + public void shouldCreateECDSA384AlgorithmWithBothKeys() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); Algorithm algorithm = Algorithm.ECDSA384(publicKey, privateKey); @@ -485,7 +485,7 @@ public void shouldCreateECDSA384AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateECDSA384AlgorithmWithProvider() throws Exception { + public void shouldCreateECDSA384AlgorithmWithProvider() { ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); Algorithm algorithm = Algorithm.ECDSA384(provider); @@ -496,7 +496,7 @@ public void shouldCreateECDSA384AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateECDSA512AlgorithmWithPublicKey() throws Exception { + public void shouldCreateECDSA512AlgorithmWithPublicKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); Algorithm algorithm = Algorithm.ECDSA512(key); @@ -507,7 +507,7 @@ public void shouldCreateECDSA512AlgorithmWithPublicKey() throws Exception { } @Test - public void shouldCreateECDSA512AlgorithmWithPrivateKey() throws Exception { + public void shouldCreateECDSA512AlgorithmWithPrivateKey() { ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); Algorithm algorithm = Algorithm.ECDSA512(key); @@ -518,7 +518,7 @@ public void shouldCreateECDSA512AlgorithmWithPrivateKey() throws Exception { } @Test - public void shouldCreateECDSA512AlgorithmWithBothKeys() throws Exception { + public void shouldCreateECDSA512AlgorithmWithBothKeys() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); Algorithm algorithm = Algorithm.ECDSA512(publicKey, privateKey); @@ -530,7 +530,7 @@ public void shouldCreateECDSA512AlgorithmWithBothKeys() throws Exception { } @Test - public void shouldCreateECDSA512AlgorithmWithProvider() throws Exception { + public void shouldCreateECDSA512AlgorithmWithProvider() { ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); Algorithm algorithm = Algorithm.ECDSA512(provider); @@ -541,7 +541,7 @@ public void shouldCreateECDSA512AlgorithmWithProvider() throws Exception { } @Test - public void shouldCreateNoneAlgorithm() throws Exception { + public void shouldCreateNoneAlgorithm() { Algorithm algorithm = Algorithm.none(); assertThat(algorithm, is(notNullValue())); 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..ef8e65e8 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.hamcrest.MatcherAssert.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 b50832d1..2e636c71 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -4,43 +4,44 @@ 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 com.auth0.jwt.interfaces.JWTVerifier; import org.hamcrest.Matchers; import org.hamcrest.collection.IsIn; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; 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; 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; +import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -@SuppressWarnings("deprecation") 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_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"; @@ -110,7 +111,7 @@ public void shouldPassECDSA256VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA256VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA256VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -151,7 +152,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)); @@ -164,7 +165,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)); @@ -178,7 +179,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)); @@ -235,7 +236,7 @@ public void shouldPassECDSA384VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA384VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA384VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -276,7 +277,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)); @@ -289,7 +290,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)); @@ -303,7 +304,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)); @@ -360,7 +361,7 @@ public void shouldPassECDSA512VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA512VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA512VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -401,7 +402,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)); @@ -414,7 +415,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)); @@ -428,7 +429,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)); @@ -443,7 +444,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"); @@ -464,6 +465,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); @@ -482,6 +487,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); @@ -500,6 +509,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); @@ -507,7 +520,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"; @@ -533,7 +557,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); @@ -556,7 +580,7 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() { 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)); @@ -614,7 +638,7 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA384withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -675,7 +699,7 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA512withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -750,7 +774,7 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -760,7 +784,7 @@ public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Ex } @Test - public void shouldReturnSigningKeyIdFromProvider() throws Exception { + public void shouldReturnSigningKeyIdFromProvider() { ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKeyId()).thenReturn("keyId"); Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); @@ -818,12 +842,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 @@ -1015,10 +1040,10 @@ public void shouldDecodeECDSA512DER() throws Exception { //Test Helpers static void assertValidJOSESignature(byte[] joseSignature, int numberSize, boolean withRPadding, boolean withSPadding) { - Assert.assertThat(joseSignature, is(Matchers.notNullValue())); - Assert.assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + assertThat(joseSignature, is(Matchers.notNullValue())); + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); - Assert.assertThat(joseSignature.length, is(numberSize * 2)); + assertThat(joseSignature.length, is(numberSize * 2)); byte[] rCopy = Arrays.copyOfRange(joseSignature, 0, numberSize); byte[] sCopy = Arrays.copyOfRange(joseSignature, numberSize, numberSize * 2); @@ -1033,12 +1058,12 @@ static void assertValidJOSESignature(byte[] joseSignature, int numberSize, boole if (withSPadding) { sNumber[0] = (byte) 0; } - Assert.assertThat(Arrays.equals(rNumber, rCopy), is(true)); - Assert.assertThat(Arrays.equals(sNumber, sCopy), is(true)); + assertThat(Arrays.equals(rNumber, rCopy), is(true)); + assertThat(Arrays.equals(sNumber, sCopy), is(true)); } static byte[] createDERSignature(int numberSize, boolean withRPadding, boolean withSPadding) { - Assert.assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); int rLength = withRPadding ? numberSize - 1 : numberSize; int sLength = withSPadding ? numberSize - 1 : numberSize; @@ -1081,7 +1106,7 @@ static byte[] createDERSignature(int numberSize, boolean withRPadding, boolean w } static byte[] createJOSESignature(int numberSize, boolean withRPadding, boolean withSPadding) { - Assert.assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); byte[] rNumber = new byte[numberSize]; byte[] sNumber = new byte[numberSize]; @@ -1100,8 +1125,8 @@ static byte[] createJOSESignature(int numberSize, boolean withRPadding, boolean } static void assertValidDERSignature(byte[] derSignature, int numberSize, boolean withRPadding, boolean withSPadding) { - Assert.assertThat(derSignature, is(Matchers.notNullValue())); - Assert.assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + assertThat(derSignature, is(Matchers.notNullValue())); + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); int rLength = withRPadding ? numberSize - 1 : numberSize; int sLength = withSPadding ? numberSize - 1 : numberSize; @@ -1109,24 +1134,24 @@ static void assertValidDERSignature(byte[] derSignature, int numberSize, boolean int offset = 0; //Start sequence - Assert.assertThat(derSignature[offset++], is((byte) 0x30)); + assertThat(derSignature[offset++], is((byte) 0x30)); if (totalLength > 0x7f) { //Add sign before sequence length totalLength++; - Assert.assertThat(derSignature[offset++], is((byte) 0x81)); + assertThat(derSignature[offset++], is((byte) 0x81)); } //Sequence length - Assert.assertThat(derSignature[offset++], is((byte) (totalLength - offset))); + assertThat(derSignature[offset++], is((byte) (totalLength - offset))); //R number - Assert.assertThat(derSignature[offset++], is((byte) 0x02)); - Assert.assertThat(derSignature[offset++], is((byte) rLength)); + assertThat(derSignature[offset++], is((byte) 0x02)); + assertThat(derSignature[offset++], is((byte) rLength)); byte[] rCopy = Arrays.copyOfRange(derSignature, offset, offset + rLength); offset += rLength; //S number - Assert.assertThat(derSignature[offset++], is((byte) 0x02)); - Assert.assertThat(derSignature[offset++], is((byte) sLength)); + assertThat(derSignature[offset++], is((byte) 0x02)); + assertThat(derSignature[offset++], is((byte) sLength)); byte[] sCopy = Arrays.copyOfRange(derSignature, offset, offset + sLength); @@ -1134,9 +1159,9 @@ static void assertValidDERSignature(byte[] derSignature, int numberSize, boolean byte[] sNumber = new byte[sLength]; Arrays.fill(rNumber, (byte) 0x11); Arrays.fill(sNumber, (byte) 0x22); - Assert.assertThat(Arrays.equals(rNumber, rCopy), is(true)); - Assert.assertThat(Arrays.equals(sNumber, sCopy), is(true)); - Assert.assertThat(derSignature.length, is(totalLength)); + assertThat(Arrays.equals(rNumber, rCopy), is(true)); + assertThat(Arrays.equals(sNumber, sCopy), is(true)); + assertThat(derSignature.length, is(totalLength)); } @Test @@ -1157,12 +1182,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)); @@ -1176,7 +1201,7 @@ public void shouldBeEqualSignatureMethodDecodeResults() throws Exception { */ @Test - public void shouldFailOnECDSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() { 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)); @@ -1188,4 +1213,140 @@ 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"); + JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); + JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); + JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); + verifier256.verify(jwtWithInvalidSig); + verifier384.verify(jwtWithInvalidSig); + verifier512.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"); + JWTVerifier verifier256 = JWT.require(Algorithm.ECDSA256(key256)).build(); + JWTVerifier verifier384 = JWT.require(Algorithm.ECDSA256(key384)).build(); + JWTVerifier verifier512 = JWT.require(Algorithm.ECDSA256(key512)).build(); + verifier256.verify(jwtWithInvalidSig); + verifier384.verify(jwtWithInvalidSig); + verifier512.verify(jwtWithInvalidSig); + } + + @Test + public void signatureWithAllZerosShouldFail() throws Exception { + exception.expect(SignatureException.class); + exception.expectMessage("Invalid signature format."); + + 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); + exception.expectMessage("Invalid signature format."); + + 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); + exception.expectMessage("Invalid signature format."); + + 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 signatureWithRValueNotLessThanOrderShouldFail() throws Exception { + exception.expect(SignatureException.class); + exception.expectMessage("Invalid signature format."); + + 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); + } + + @Test + public void signatureWithSValueNotLessThanOrderShouldFail() throws Exception { + exception.expect(SignatureException.class); + exception.expectMessage("Invalid signature format."); + + 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 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..3925eef3 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; @@ -13,18 +11,24 @@ 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; 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.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -44,7 +48,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(); @@ -56,23 +62,21 @@ public class ECDSABouncyCastleProviderTests { //These tests add and use the BouncyCastle SecurityProvider to handle ECDSA algorithms @BeforeClass - public static void setUp() throws Exception { + public static void setUp() { //Set BC as the preferred bcProvider Security.insertProviderAt(bcProvider, 1); } @AfterClass - public static void tearDown() throws Exception { + public static void tearDown() { Security.removeProvider(bcProvider.getName()); } @Test - public void shouldPreferBouncyCastleProvider() throws Exception { + public void shouldPreferBouncyCastleProvider() { assertThat(Security.getProviders()[0], is(equalTo(bcProvider))); } - - // Verify - + @Test public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; @@ -88,7 +92,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)); @@ -108,7 +112,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)); } @@ -124,7 +128,7 @@ public void shouldPassECDSA256VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA256VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA256VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -165,7 +169,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)); @@ -178,7 +182,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)); @@ -192,7 +196,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)); @@ -213,7 +217,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)); @@ -233,7 +237,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)); } @@ -249,7 +253,7 @@ public void shouldPassECDSA384VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA384VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA384VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -290,7 +294,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)); @@ -303,7 +307,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)); @@ -317,7 +321,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)); @@ -338,7 +342,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)); @@ -358,7 +362,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)); } @@ -374,7 +378,7 @@ public void shouldPassECDSA512VerificationWithProvidedPublicKey() throws Excepti } @Test - public void shouldFailECDSA512VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailECDSA512VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -415,7 +419,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)); @@ -428,7 +432,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)); @@ -442,7 +446,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)); @@ -457,7 +461,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"); @@ -478,6 +482,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); @@ -496,6 +504,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); @@ -514,6 +526,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); @@ -562,7 +578,7 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() { 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)); @@ -620,7 +636,7 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA384withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -679,7 +695,7 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA512withECDSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -754,7 +770,7 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -764,7 +780,7 @@ public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Ex } @Test - public void shouldReturnSigningKeyIdFromProvider() throws Exception { + public void shouldReturnSigningKeyIdFromProvider() { ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKeyId()).thenReturn("keyId"); Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); @@ -822,12 +838,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 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..9b6ac0c0 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -9,17 +9,17 @@ 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; -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; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -33,14 +33,26 @@ public class HMACAlgorithmTest { // Verify @Test - public void shouldGetStringBytes() throws Exception { + public void shouldGetStringBytes() { String text = "abcdef123456!@#$%^"; - byte[] expectedBytes = text.getBytes("UTF-8"); - assertTrue(Arrays.equals(expectedBytes, HMACAlgorithm.getSecretBytes(text))); + byte[] expectedBytes = text.getBytes(StandardCharsets.UTF_8); + assertArrayEquals(expectedBytes, HMACAlgorithm.getSecretBytes(text)); } @Test - public void shouldPassHMAC256Verification() throws Exception { + public void shouldCopyTheReceivedSecretArray() { + 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() { String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; Algorithm algorithmString = Algorithm.HMAC256("secret"); Algorithm algorithmBytes = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); @@ -50,7 +62,7 @@ public void shouldPassHMAC256Verification() throws Exception { } @Test - public void shouldFailHMAC256VerificationWithInvalidSecretString() throws Exception { + public void shouldFailHMAC256VerificationWithInvalidSecretString() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"); String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; @@ -59,7 +71,7 @@ public void shouldFailHMAC256VerificationWithInvalidSecretString() throws Except } @Test - public void shouldFailHMAC256VerificationWithInvalidSecretBytes() throws Exception { + public void shouldFailHMAC256VerificationWithInvalidSecretBytes() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256"); String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; @@ -68,7 +80,7 @@ public void shouldFailHMAC256VerificationWithInvalidSecretBytes() throws Excepti } @Test - public void shouldPassHMAC384Verification() throws Exception { + public void shouldPassHMAC384Verification() { String jwt = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; Algorithm algorithmString = Algorithm.HMAC384("secret"); Algorithm algorithmBytes = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); @@ -78,7 +90,7 @@ public void shouldPassHMAC384Verification() throws Exception { } @Test - public void shouldFailHMAC384VerificationWithInvalidSecretString() throws Exception { + public void shouldFailHMAC384VerificationWithInvalidSecretString() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA384"); String jwt = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; @@ -87,7 +99,7 @@ public void shouldFailHMAC384VerificationWithInvalidSecretString() throws Except } @Test - public void shouldFailHMAC384VerificationWithInvalidSecretBytes() throws Exception { + public void shouldFailHMAC384VerificationWithInvalidSecretBytes() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA384"); String jwt = "eyJhbGciOiJIUzM4NCIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.uztpK_wUMYJhrRv8SV-1LU4aPnwl-EM1q-wJnqgyb5DHoDteP6lN_gE1xnZJH5vw"; @@ -96,7 +108,7 @@ public void shouldFailHMAC384VerificationWithInvalidSecretBytes() throws Excepti } @Test - public void shouldPassHMAC512Verification() throws Exception { + public void shouldPassHMAC512Verification() { String jwt = "eyJhbGciOiJIUzUxMiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.VUo2Z9SWDV-XcOc_Hr6Lff3vl7L9e5Vb8ThXpmGDFjHxe3Dr1ZBmUChYF-xVA7cAdX1P_D4ZCUcsv3IefpVaJw"; Algorithm algorithmString = Algorithm.HMAC512("secret"); Algorithm algorithmBytes = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); @@ -106,7 +118,7 @@ public void shouldPassHMAC512Verification() throws Exception { } @Test - public void shouldFailHMAC512VerificationWithInvalidSecretString() throws Exception { + public void shouldFailHMAC512VerificationWithInvalidSecretString() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA512"); String jwt = "eyJhbGciOiJIUzUxMiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.VUo2Z9SWDV-XcOc_Hr6Lff3vl7L9e5Vb8ThXpmGDFjHxe3Dr1ZBmUChYF-xVA7cAdX1P_D4ZCUcsv3IefpVaJw"; @@ -115,7 +127,7 @@ public void shouldFailHMAC512VerificationWithInvalidSecretString() throws Except } @Test - public void shouldFailHMAC512VerificationWithInvalidSecretBytes() throws Exception { + public void shouldFailHMAC512VerificationWithInvalidSecretBytes() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA512"); String jwt = "eyJhbGciOiJIUzUxMiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.VUo2Z9SWDV-XcOc_Hr6Lff3vl7L9e5Vb8ThXpmGDFjHxe3Dr1ZBmUChYF-xVA7cAdX1P_D4ZCUcsv3IefpVaJw"; @@ -161,7 +173,7 @@ public void shouldThrowOnVerifyWhenTheSecretIsInvalid() throws Exception { private static final String auth0IssPayload = "eyJpc3MiOiJhdXRoMCJ9"; @Test - public void shouldDoHMAC256SigningWithBytes() throws Exception { + public void shouldDoHMAC256SigningWithBytes() { Algorithm algorithm = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); @@ -173,7 +185,7 @@ public void shouldDoHMAC256SigningWithBytes() throws Exception { } @Test - public void shouldDoHMAC384SigningWithBytes() throws Exception { + public void shouldDoHMAC384SigningWithBytes() { Algorithm algorithm = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); @@ -185,7 +197,7 @@ public void shouldDoHMAC384SigningWithBytes() throws Exception { } @Test - public void shouldDoHMAC512SigningWithBytes() throws Exception { + public void shouldDoHMAC512SigningWithBytes() { Algorithm algorithm = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); String jwt = asJWT(algorithm, HS512Header, auth0IssPayload); @@ -197,7 +209,7 @@ public void shouldDoHMAC512SigningWithBytes() throws Exception { } @Test - public void shouldDoHMAC256SigningWithString() throws Exception { + public void shouldDoHMAC256SigningWithString() { Algorithm algorithm = Algorithm.HMAC256("secret"); String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); @@ -209,7 +221,7 @@ public void shouldDoHMAC256SigningWithString() throws Exception { } @Test - public void shouldDoHMAC384SigningWithString() throws Exception { + public void shouldDoHMAC384SigningWithString() { Algorithm algorithm = Algorithm.HMAC384("secret"); String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); @@ -221,7 +233,7 @@ public void shouldDoHMAC384SigningWithString() throws Exception { } @Test - public void shouldDoHMAC512SigningWithString() throws Exception { + public void shouldDoHMAC512SigningWithString() { Algorithm algorithm = Algorithm.HMAC512("secret"); String jwt = asJWT(algorithm ,HS512Header, auth0IssPayload); @@ -261,7 +273,7 @@ public void shouldThrowOnSignWhenTheSecretIsInvalid() throws Exception { } @Test - public void shouldReturnNullSigningKeyId() throws Exception { + public void shouldReturnNullSigningKeyId() { assertThat(Algorithm.HMAC256("secret").getSigningKeyId(), is(nullValue())); } @@ -280,4 +292,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..263dc249 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java @@ -7,9 +7,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.MatcherAssert.assertThat; public class NoneAlgorithmTest { @@ -17,14 +16,14 @@ public class NoneAlgorithmTest { public ExpectedException exception = ExpectedException.none(); @Test - public void shouldPassNoneVerification() throws Exception { + public void shouldPassNoneVerification() { Algorithm algorithm = Algorithm.none(); String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9."; algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldFailNoneVerificationWhenTokenHasTwoParts() throws Exception { + public void shouldFailNoneVerificationWhenTokenHasTwoParts() { exception.expect(JWTDecodeException.class); exception.expectMessage("The token was expected to have 3 parts, but got 2."); String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9"; @@ -33,7 +32,7 @@ public void shouldFailNoneVerificationWhenTokenHasTwoParts() throws Exception { } @Test - public void shouldFailNoneVerificationWhenSignatureIsPresent() throws Exception { + public void shouldFailNoneVerificationWhenSignatureIsPresent() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: none"); String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9.Ox-WRXRaGAuWt2KfPvWiGcCrPqZtbp_4OnQzZXaTfss"; @@ -42,7 +41,17 @@ public void shouldFailNoneVerificationWhenSignatureIsPresent() throws Exception } @Test - public void shouldReturnNullSigningKeyId() throws Exception { + public void shouldReturnNullSigningKeyId() { 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..115de64c 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -17,7 +17,7 @@ import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -25,7 +25,6 @@ import static org.mockito.Mockito.when; import static com.auth0.jwt.algorithms.CryptoTestHelper.*; -@SuppressWarnings("deprecation") public class RSAAlgorithmTest { private static final String PRIVATE_KEY_FILE = "src/test/resources/rsa-private.pem"; @@ -62,7 +61,7 @@ public void shouldPassRSA256VerificationWithProvidedPublicKey() throws Exception } @Test - public void shouldFailRSA256VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailRSA256VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -119,7 +118,7 @@ public void shouldPassRSA384VerificationWithProvidedPublicKey() throws Exception } @Test - public void shouldFailRSA384VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailRSA384VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withRSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -176,7 +175,7 @@ public void shouldPassRSA512VerificationWithProvidedPublicKey() throws Exception } @Test - public void shouldFailRSA512VerificationWhenProvidedPublicKeyIsNull() throws Exception { + public void shouldFailRSA512VerificationWhenProvidedPublicKeyIsNull() { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withRSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -310,7 +309,7 @@ public void shouldDoRSA256SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull() { 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)); @@ -374,7 +373,7 @@ public void shouldDoRSA384SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnRSA384SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnRSA384SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA384withRSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -438,7 +437,7 @@ public void shouldDoRSA512SigningWithProvidedPrivateKey() throws Exception { } @Test - public void shouldFailOnRSA512SigningWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnRSA512SigningWhenProvidedPrivateKeyIsNull() { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA512withRSA"); exception.expectCause(isA(IllegalStateException.class)); @@ -513,7 +512,7 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -523,7 +522,7 @@ public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Ex } @Test - public void shouldReturnSigningKeyIdFromProvider() throws Exception { + public void shouldReturnSigningKeyIdFromProvider() { RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKeyId()).thenReturn("keyId"); Algorithm algorithm = new RSAAlgorithm("some-alg", "some-algorithm", provider); @@ -553,11 +552,10 @@ public void shouldBeEqualSignatureMethodResults() throws Exception { * Test deprecated signing method error handling. * * @see {@linkplain #shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull} - * @throws Exception expected exception */ @Test - public void shouldFailOnRSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() throws Exception { + public void shouldFailOnRSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() { 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)); @@ -569,4 +567,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 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 c2d89556..c4a04d81 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java @@ -25,21 +25,21 @@ public class BasicHeaderTest { @SuppressWarnings("Convert2Diamond") @Test - public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNonNullTree() throws Exception { + public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNonNullTree() { exception.expect(UnsupportedOperationException.class); BasicHeader header = new BasicHeader(null, null, null, null, new HashMap(), objectReader); header.getTree().put("something", null); } @Test - public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNullTree() throws Exception { + public void shouldHaveUnmodifiableTreeWhenInstantiatedWithNullTree() { exception.expect(UnsupportedOperationException.class); BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); header.getTree().put("something", null); } @Test - public void shouldHaveTree() throws Exception { + public void shouldHaveTree() { HashMap map = new HashMap<>(); JsonNode node = NullNode.getInstance(); map.put("key", node); @@ -50,7 +50,7 @@ public void shouldHaveTree() throws Exception { } @Test - public void shouldGetAlgorithm() throws Exception { + public void shouldGetAlgorithm() { BasicHeader header = new BasicHeader("HS256", null, null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -59,7 +59,7 @@ public void shouldGetAlgorithm() throws Exception { } @Test - public void shouldGetNullAlgorithmIfMissing() throws Exception { + public void shouldGetNullAlgorithmIfMissing() { BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -67,7 +67,7 @@ public void shouldGetNullAlgorithmIfMissing() throws Exception { } @Test - public void shouldGetType() throws Exception { + public void shouldGetType() { BasicHeader header = new BasicHeader(null, "jwt", null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -76,7 +76,7 @@ public void shouldGetType() throws Exception { } @Test - public void shouldGetNullTypeIfMissing() throws Exception { + public void shouldGetNullTypeIfMissing() { BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -84,7 +84,7 @@ public void shouldGetNullTypeIfMissing() throws Exception { } @Test - public void shouldGetContentType() throws Exception { + public void shouldGetContentType() { BasicHeader header = new BasicHeader(null, null, "content", null, null, objectReader); assertThat(header, is(notNullValue())); @@ -93,7 +93,7 @@ public void shouldGetContentType() throws Exception { } @Test - public void shouldGetNullContentTypeIfMissing() throws Exception { + public void shouldGetNullContentTypeIfMissing() { BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -101,7 +101,7 @@ public void shouldGetNullContentTypeIfMissing() throws Exception { } @Test - public void shouldGetKeyId() throws Exception { + public void shouldGetKeyId() { BasicHeader header = new BasicHeader(null, null, null, "key", null, objectReader); assertThat(header, is(notNullValue())); @@ -110,7 +110,7 @@ public void shouldGetKeyId() throws Exception { } @Test - public void shouldGetNullKeyIdIfMissing() throws Exception { + public void shouldGetNullKeyIdIfMissing() { BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); @@ -118,7 +118,7 @@ public void shouldGetNullKeyIdIfMissing() throws Exception { } @Test - public void shouldGetExtraClaim() throws Exception { + public void shouldGetExtraClaim() { Map tree = new HashMap<>(); tree.put("extraClaim", new TextNode("extraValue")); BasicHeader header = new BasicHeader(null, null, null, null, tree, objectReader); @@ -129,12 +129,13 @@ public void shouldGetExtraClaim() throws Exception { } @Test - public void shouldGetNotNullExtraClaimIfMissing() throws Exception { + public void shouldGetNotNullExtraClaimIfMissing() { Map tree = new HashMap<>(); BasicHeader header = new BasicHeader(null, null, null, null, tree, objectReader); 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/HeaderDeserializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java index dffcab50..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.junit.Assert.assertThat; +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() throws Exception { - deserializer = new HeaderDeserializer(objectReader); + public void setUp() { + 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); @@ -88,7 +88,7 @@ public void shouldNotRemoveKnownPublicClaimsFromTree() throws Exception { } @Test - public void shouldGetNullStringWhenParsingNullNode() throws Exception { + public void shouldGetNullStringWhenParsingNullNode() { Map tree = new HashMap<>(); NullNode node = NullNode.getInstance(); tree.put("key", node); @@ -98,7 +98,7 @@ public void shouldGetNullStringWhenParsingNullNode() throws Exception { } @Test - public void shouldGetNullStringWhenParsingNull() throws Exception { + public void shouldGetNullStringWhenParsingNull() { Map tree = new HashMap<>(); tree.put("key", null); @@ -107,7 +107,7 @@ public void shouldGetNullStringWhenParsingNull() throws Exception { } @Test - public void shouldGetStringWhenParsingTextNode() throws Exception { + public void shouldGetStringWhenParsingTextNode() { Map tree = new HashMap<>(); TextNode node = new TextNode("something here"); tree.put("key", node); 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 4f97b2bd..da62131a 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java @@ -27,12 +27,12 @@ public class JWTParserTest { private JWTParser parser; @Before - public void setUp() throws Exception { + public void setUp() { parser = new JWTParser(); } @Test - public void shouldGetDefaultObjectMapper() throws Exception { + public void shouldGetDefaultObjectMapper() { ObjectMapper mapper = getDefaultObjectMapper(); assertThat(mapper, is(notNullValue())); assertThat(mapper, is(instanceOf(ObjectMapper.class))); @@ -40,9 +40,9 @@ public void shouldGetDefaultObjectMapper() throws Exception { } @Test - public void shouldAddDeserializers() throws Exception { + public void shouldAddDeserializers() { ObjectMapper mapper = mock(ObjectMapper.class); - new JWTParser(mapper); + JWTParser.addDeserializers(mapper); verify(mapper).registerModule(any(Module.class)); } @@ -58,7 +58,7 @@ public void shouldParsePayload() throws Exception { } @Test - public void shouldThrowOnInvalidPayload() throws Exception { + public void shouldThrowOnInvalidPayload() { String jsonPayload = "{{"; exception.expect(JWTDecodeException.class); exception.expectMessage(String.format("The string '%s' doesn't have a valid JSON format.", jsonPayload)); @@ -78,7 +78,7 @@ public void shouldParseHeader() throws Exception { } @Test - public void shouldThrowOnInvalidHeader() throws Exception { + public void shouldThrowOnInvalidHeader() { String jsonHeader = "}}"; exception.expect(JWTDecodeException.class); exception.expectMessage(String.format("The string '%s' doesn't have a valid JSON format.", jsonHeader)); @@ -87,28 +87,28 @@ public void shouldThrowOnInvalidHeader() throws Exception { } @Test - public void shouldThrowWhenConvertingHeaderIfNullJson() throws Exception { + public void shouldThrowWhenConvertingHeaderIfNullJson() { 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 { + public void shouldThrowWhenConvertingHeaderFromInvalidJson() { exception.expect(JWTDecodeException.class); exception.expectMessage("The string '}{' doesn't have a valid JSON format."); parser.parseHeader("}{"); } @Test - public void shouldThrowWhenConvertingPayloadIfNullJson() throws Exception { + public void shouldThrowWhenConvertingPayloadIfNullJson() { exception.expect(JWTDecodeException.class); exception.expectMessage("The string 'null' doesn't have a valid JSON format."); parser.parsePayload(null); } @Test - public void shouldThrowWhenConvertingPayloadFromInvalidJson() throws Exception { + public void shouldThrowWhenConvertingPayloadFromInvalidJson() { exception.expect(JWTDecodeException.class); exception.expectMessage("The string '}{' doesn't have a valid JSON format."); parser.parsePayload("}{"); 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..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; @@ -20,32 +19,43 @@ import org.mockito.ArgumentMatchers; import java.io.IOException; -import java.util.*; +import java.time.Instant; +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 com.auth0.jwt.impl.JsonNodeClaim.claimFromNode; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +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.assertThat; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertNull; +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(); @Before - public void setUp() throws Exception { + public void setUp() { mapper = getDefaultObjectMapper(); - objectReader = mapper.reader(); } @Test - public void shouldGetBooleanValue() throws Exception { + public void shouldGetBooleanValue() { JsonNode value = mapper.valueToTree(true); Claim claim = claimFromNode(value); @@ -54,11 +64,11 @@ public void shouldGetBooleanValue() throws Exception { } private Claim claimFromNode(JsonNode value) { - return JsonNodeClaim.claimFromNode(value, objectReader); + return JsonNodeClaim.claimFromNode(value, mapper); } @Test - public void shouldGetNullBooleanIfNotBooleanValue() throws Exception { + public void shouldGetNullBooleanIfNotBooleanValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asBoolean(), is(nullValue())); JsonNode stringValue = mapper.valueToTree("boolean"); @@ -66,7 +76,7 @@ public void shouldGetNullBooleanIfNotBooleanValue() throws Exception { } @Test - public void shouldGetIntValue() throws Exception { + public void shouldGetIntValue() { JsonNode value = mapper.valueToTree(123); Claim claim = claimFromNode(value); @@ -75,7 +85,7 @@ public void shouldGetIntValue() throws Exception { } @Test - public void shouldGetNullIntIfNotIntValue() throws Exception { + public void shouldGetNullIntIfNotIntValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asInt(), is(nullValue())); JsonNode stringValue = mapper.valueToTree("123"); @@ -83,7 +93,7 @@ public void shouldGetNullIntIfNotIntValue() throws Exception { } @Test - public void shouldGetLongValue() throws Exception { + public void shouldGetLongValue() { JsonNode value = mapper.valueToTree(Long.MAX_VALUE); Claim claim = claimFromNode(value); @@ -92,7 +102,7 @@ public void shouldGetLongValue() throws Exception { } @Test - public void shouldGetNullLongIfNotIntValue() throws Exception { + public void shouldGetNullLongIfNotIntValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asLong(), is(nullValue())); JsonNode stringValue = mapper.valueToTree("" + Long.MAX_VALUE); @@ -100,7 +110,7 @@ public void shouldGetNullLongIfNotIntValue() throws Exception { } @Test - public void shouldGetDoubleValue() throws Exception { + public void shouldGetDoubleValue() { JsonNode value = mapper.valueToTree(1.5); Claim claim = claimFromNode(value); @@ -109,7 +119,7 @@ public void shouldGetDoubleValue() throws Exception { } @Test - public void shouldGetNullDoubleIfNotDoubleValue() throws Exception { + public void shouldGetNullDoubleIfNotDoubleValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asDouble(), is(nullValue())); JsonNode stringValue = mapper.valueToTree("123.23"); @@ -117,24 +127,27 @@ public void shouldGetNullDoubleIfNotDoubleValue() throws Exception { } @Test - public void shouldGetDateValue() throws Exception { - JsonNode value = mapper.valueToTree(1476824844L); + public void shouldGetNumericDateValue() { + long seconds = 1476824844L; + JsonNode value = mapper.valueToTree(seconds); Claim claim = claimFromNode(value); - assertThat(claim.asDate(), is(notNullValue())); - assertThat(claim.asDate(), is(new Date(1476824844L * 1000))); + assertThat(claim.asDate(), is(new Date(seconds * 1000))); + assertThat(claim.asInstant(), is(Instant.ofEpochSecond(seconds))); } @Test - public void shouldGetNullDateIfNotDateValue() throws Exception { + public void shouldGetNullIfNotNumericDateValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asDate(), is(nullValue())); + assertThat(claimFromNode(objectValue).asInstant(), is(nullValue())); JsonNode stringValue = mapper.valueToTree("1476824844"); assertThat(claimFromNode(stringValue).asDate(), is(nullValue())); + assertThat(claimFromNode(stringValue).asInstant(), is(nullValue())); } @Test - public void shouldGetStringValue() throws Exception { + public void shouldGetStringValue() { JsonNode value = mapper.valueToTree("string"); Claim claim = claimFromNode(value); @@ -143,7 +156,7 @@ public void shouldGetStringValue() throws Exception { } @Test - public void shouldGetNullStringIfNotStringValue() throws Exception { + public void shouldGetNullStringIfNotStringValue() { JsonNode objectValue = mapper.valueToTree(new Object()); assertThat(claimFromNode(objectValue).asString(), is(nullValue())); JsonNode intValue = mapper.valueToTree(12345); @@ -151,7 +164,7 @@ public void shouldGetNullStringIfNotStringValue() throws Exception { } @Test - public void shouldGetArrayValueOfCustomClass() throws Exception { + public void shouldGetArrayValueOfCustomClass() { JsonNode value = mapper.valueToTree(new UserPojo[]{new UserPojo("George", 1), new UserPojo("Mark", 2)}); Claim claim = claimFromNode(value); @@ -160,7 +173,7 @@ public void shouldGetArrayValueOfCustomClass() throws Exception { } @Test - public void shouldGetArrayValue() throws Exception { + public void shouldGetArrayValue() { JsonNode value = mapper.valueToTree(new String[]{"string1", "string2"}); Claim claim = claimFromNode(value); @@ -169,7 +182,7 @@ public void shouldGetArrayValue() throws Exception { } @Test - public void shouldGetNullArrayIfNullValue() throws Exception { + public void shouldGetNullArrayIfNullValue() { JsonNode value = mapper.valueToTree(null); Claim claim = claimFromNode(value); @@ -177,7 +190,7 @@ public void shouldGetNullArrayIfNullValue() throws Exception { } @Test - public void shouldGetNullArrayIfNonArrayValue() throws Exception { + public void shouldGetNullArrayIfNonArrayValue() { JsonNode value = mapper.valueToTree(1); Claim claim = claimFromNode(value); @@ -185,7 +198,7 @@ public void shouldGetNullArrayIfNonArrayValue() throws Exception { } @Test - public void shouldThrowIfArrayClassMismatch() throws Exception { + public void shouldThrowIfArrayClassMismatch() { JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); Claim claim = claimFromNode(value); @@ -194,7 +207,7 @@ public void shouldThrowIfArrayClassMismatch() throws Exception { } @Test - public void shouldGetListValueOfCustomClass() throws Exception { + public void shouldGetListValueOfCustomClass() { JsonNode value = mapper.valueToTree(Arrays.asList(new UserPojo("George", 1), new UserPojo("Mark", 2))); Claim claim = claimFromNode(value); @@ -203,7 +216,7 @@ public void shouldGetListValueOfCustomClass() throws Exception { } @Test - public void shouldGetListValue() throws Exception { + public void shouldGetListValue() { JsonNode value = mapper.valueToTree(Arrays.asList("string1", "string2")); Claim claim = claimFromNode(value); @@ -212,7 +225,7 @@ public void shouldGetListValue() throws Exception { } @Test - public void shouldGetNullListIfNullValue() throws Exception { + public void shouldGetNullListIfNullValue() { JsonNode value = mapper.valueToTree(null); Claim claim = claimFromNode(value); @@ -220,7 +233,7 @@ public void shouldGetNullListIfNullValue() throws Exception { } @Test - public void shouldGetNullListIfNonArrayValue() throws Exception { + public void shouldGetNullListIfNonArrayValue() { JsonNode value = mapper.valueToTree(1); Claim claim = claimFromNode(value); @@ -228,7 +241,7 @@ public void shouldGetNullListIfNonArrayValue() throws Exception { } @Test - public void shouldThrowIfListClassMismatch() throws Exception { + public void shouldThrowIfListClassMismatch() { JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); Claim claim = claimFromNode(value); @@ -237,7 +250,7 @@ public void shouldThrowIfListClassMismatch() throws Exception { } @Test - public void shouldGetNullMapIfNullValue() throws Exception { + public void shouldGetNullMapIfNullValue() { JsonNode value = mapper.valueToTree(null); Claim claim = claimFromNode(value); @@ -245,7 +258,7 @@ public void shouldGetNullMapIfNullValue() throws Exception { } @Test - public void shouldGetNullMapIfNonArrayValue() throws Exception { + public void shouldGetNullMapIfNonArrayValue() { JsonNode value = mapper.valueToTree(1); Claim claim = claimFromNode(value); @@ -253,7 +266,7 @@ public void shouldGetNullMapIfNonArrayValue() throws Exception { } @Test - public void shouldGetMapValue() throws Exception { + public void shouldGetMapValue() { Map map = new HashMap<>(); map.put("text", "extraValue"); map.put("number", 12); @@ -278,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); @@ -292,7 +305,7 @@ public void shouldThrowIfAnExtraordinaryExceptionHappensWhenParsingAsGenericMap( } @Test - public void shouldGetCustomClassValue() throws Exception { + public void shouldGetCustomClassValue() { JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); Claim claim = claimFromNode(value); @@ -302,7 +315,7 @@ public void shouldGetCustomClassValue() throws Exception { } @Test - public void shouldThrowIfCustomClassMismatch() throws Exception { + public void shouldThrowIfCustomClassMismatch() { JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); Claim claim = claimFromNode(value); @@ -310,9 +323,51 @@ public void shouldThrowIfCustomClassMismatch() throws Exception { claim.as(String.class); } + @Test + public void shouldReturnNullForMissingAndNullClaims() { + JsonNode missingValue = MissingNode.getInstance(); + Claim missingClaim = claimFromNode(missingValue); + assertThat(missingClaim.isMissing(), is(true)); + assertThat(missingClaim.isNull(), is(false)); + assertNull(missingClaim.as(String.class)); + assertNull(missingClaim.asString()); + assertNull(missingClaim.asBoolean()); + assertNull(missingClaim.asDate()); + assertNull(missingClaim.asDouble()); + assertNull(missingClaim.asLong()); + assertNull(missingClaim.asInt()); + assertNull(missingClaim.asInstant()); + assertNull(missingClaim.asMap()); + assertNull(missingClaim.asList(String.class)); + assertNull(missingClaim.asArray(String.class)); + + JsonNode nullValue = mapper.valueToTree(null); + Claim nullClaim = claimFromNode(nullValue); + assertThat(nullClaim.isMissing(), is(false)); + assertThat(nullClaim.isNull(), is(true)); + assertNull(nullClaim.as(String.class)); + assertNull(nullClaim.asString()); + assertNull(nullClaim.asBoolean()); + assertNull(nullClaim.asDate()); + assertNull(nullClaim.asDouble()); + assertNull(nullClaim.asLong()); + assertNull(nullClaim.asInt()); + assertNull(nullClaim.asInstant()); + assertNull(nullClaim.asMap()); + assertNull(nullClaim.asList(String.class)); + assertNull(nullClaim.asArray(String.class)); + } + + @Test + public void shouldReturnNullForInvalidArrayValue() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + Claim claim = claimFromNode(value); + assertNull(claim.asArray(String.class)); + } + @SuppressWarnings({"unchecked", "RedundantCast"}) @Test - public void shouldGetAsMapValue() throws Exception { + public void shouldGetAsMapValue() { JsonNode value = mapper.valueToTree(Collections.singletonMap("key", new UserPojo("john", 123))); Claim claim = claimFromNode(value); @@ -323,112 +378,156 @@ public void shouldGetAsMapValue() throws Exception { } @Test - public void shouldReturnBaseClaimWhenParsingMissingNode() throws Exception { + public void shouldReturnBaseClaimWhenParsingMissingNode() { JsonNode value = MissingNode.getInstance(); 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 - public void shouldReturnBaseClaimWhenParsingNullNode() throws Exception { + public void shouldReturnBaseClaimWhenParsingNullNode() { JsonNode value = NullNode.getInstance(); 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 - public void shouldReturnBaseClaimWhenParsingNullValue() throws Exception { + public void shouldReturnBaseClaimWhenParsingNullValue() { JsonNode value = mapper.valueToTree(null); 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 - public void shouldReturnNonNullClaimWhenParsingObject() throws Exception { + public void shouldReturnNonNullClaimWhenParsingObject() { JsonNode value = mapper.valueToTree(new Object()); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingArray() throws Exception { + public void shouldReturnNonNullClaimWhenParsingArray() { JsonNode value = mapper.valueToTree(new String[]{}); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingList() throws Exception { + public void shouldReturnNonNullClaimWhenParsingList() { JsonNode value = mapper.valueToTree(new ArrayList()); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingStringValue() throws Exception { + public void shouldReturnNonNullClaimWhenParsingStringValue() { JsonNode value = mapper.valueToTree(""); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingIntValue() throws Exception { + public void shouldReturnNonNullClaimWhenParsingIntValue() { JsonNode value = mapper.valueToTree(Integer.MAX_VALUE); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingDoubleValue() throws Exception { + public void shouldReturnNonNullClaimWhenParsingDoubleValue() { JsonNode value = mapper.valueToTree(Double.MAX_VALUE); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingDateValue() throws Exception { + public void shouldReturnNonNullClaimWhenParsingDateValue() { JsonNode value = mapper.valueToTree(new Date()); Claim claim = claimFromNode(value); assertThat(claim, is(notNullValue())); assertThat(claim, is(instanceOf(JsonNodeClaim.class))); assertThat(claim.isNull(), is(false)); + assertThat(claim.isMissing(), is(false)); } @Test - public void shouldReturnNonNullClaimWhenParsingBooleanValue() throws Exception { + public void shouldReturnNonNullClaimWhenParsingBooleanValue() { JsonNode value = mapper.valueToTree(Boolean.TRUE); Claim claim = claimFromNode(value); 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 + public void shouldDelegateToJsonNodeToString() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + 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 d8ddd516..00000000 --- a/lib/src/test/java/com/auth0/jwt/impl/NullClaimTest.java +++ /dev/null @@ -1,73 +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.junit.Assert.assertThat; - -public class NullClaimTest { - private NullClaim claim; - - @Before - public void setUp() throws Exception { - claim = new NullClaim(); - } - - @Test - public void shouldBeNull() throws Exception { - assertThat(claim.isNull(), is(true)); - } - - @Test - public void shouldGetAsBoolean() throws Exception { - assertThat(claim.asBoolean(), is(nullValue())); - } - - @Test - public void shouldGetAsInt() throws Exception { - assertThat(claim.asInt(), is(nullValue())); - } - - @Test - public void shouldGetAsLong() throws Exception { - assertThat(claim.asLong(), is(nullValue())); - } - - @Test - public void shouldGetAsDouble() throws Exception { - assertThat(claim.asDouble(), is(nullValue())); - } - - @Test - public void shouldGetAsString() throws Exception { - assertThat(claim.asString(), is(nullValue())); - } - - @Test - public void shouldGetAsDate() throws Exception { - assertThat(claim.asDate(), is(nullValue())); - } - - @Test - public void shouldGetAsArray() throws Exception { - assertThat(claim.asArray(Object.class), is(nullValue())); - } - - @Test - public void shouldGetAsList() throws Exception { - assertThat(claim.asList(Object.class), is(nullValue())); - } - - @Test - public void shouldGetAsMap() throws Exception { - assertThat(claim.asMap(), is(nullValue())); - } - - @Test - public void shouldGetAsCustomClass() throws Exception { - assertThat(claim.as(Object.class), is(nullValue())); - } - -} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/ClaimsHolderTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadClaimsHolderTest.java similarity index 66% rename from lib/src/test/java/com/auth0/jwt/impl/ClaimsHolderTest.java rename to lib/src/test/java/com/auth0/jwt/impl/PayloadClaimsHolderTest.java index 87cc9800..2f053830 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/ClaimsHolderTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadClaimsHolderTest.java @@ -7,25 +7,24 @@ import java.util.Map; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; -public class ClaimsHolderTest { +public class PayloadClaimsHolderTest { - @SuppressWarnings("RedundantCast") @Test - public void shouldGetClaims() throws Exception { + public void shouldGetClaims() { HashMap claims = new HashMap<>(); claims.put("iss", "auth0"); - ClaimsHolder holder = new ClaimsHolder(claims); + ClaimsHolder holder = new PayloadClaimsHolder(claims); assertThat(holder, is(notNullValue())); assertThat(holder.getClaims(), is(notNullValue())); assertThat(holder.getClaims(), is(instanceOf(Map.class))); - assertThat(holder.getClaims(), is(IsMapContaining.hasEntry("iss", (Object) "auth0"))); + assertThat(holder.getClaims(), is(IsMapContaining.hasEntry("iss", "auth0"))); } @Test - public void shouldGetNotNullClaims() throws Exception { - ClaimsHolder holder = new ClaimsHolder(null); + public void shouldGetNotNullClaims() { + ClaimsHolder holder = new PayloadClaimsHolder(null); assertThat(holder, is(notNullValue())); assertThat(holder.getClaims(), is(notNullValue())); assertThat(holder.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 9a82878e..c3e04013 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadDeserializerTest.java @@ -12,17 +12,18 @@ import com.fasterxml.jackson.databind.node.*; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.collection.IsEmptyCollection; -import org.hamcrest.core.IsCollectionContaining; +import org.hamcrest.core.IsIterableContaining; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.io.StringReader; +import java.time.Instant; import java.util.*; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -34,9 +35,12 @@ public class PayloadDeserializerTest { public ExpectedException exception = ExpectedException.none(); private PayloadDeserializer deserializer; + private ObjectMapper objectMapper; + @Before - public void setUp() throws Exception { - deserializer = new PayloadDeserializer(new ObjectMapper().reader()); + public void setUp() { + objectMapper = new ObjectMapper(); + deserializer = new PayloadDeserializer(); } @Test @@ -67,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 @@ -92,10 +96,13 @@ public void shouldNotRemoveKnownPublicClaimsFromTree() throws Exception { assertThat(payload, is(notNullValue())); assertThat(payload.getIssuer(), is("auth0")); assertThat(payload.getSubject(), is("emails")); - assertThat(payload.getAudience(), is(IsCollectionContaining.hasItem("users"))); + assertThat(payload.getAudience(), is(IsIterableContaining.hasItem("users"))); assertThat(payload.getIssuedAt().getTime(), is(10101010L * 1000)); assertThat(payload.getExpiresAt().getTime(), is(11111111L * 1000)); assertThat(payload.getNotBefore().getTime(), is(10101011L * 1000)); + assertThat(payload.getIssuedAtAsInstant().getEpochSecond(), is(10101010L)); + assertThat(payload.getExpiresAtAsInstant().getEpochSecond(), is(11111111L)); + assertThat(payload.getNotBeforeAsInstant().getEpochSecond(), is(10101011L)); assertThat(payload.getId(), is("idid")); assertThat(payload.getClaim("roles").asString(), is("admin")); @@ -109,7 +116,7 @@ public void shouldNotRemoveKnownPublicClaimsFromTree() throws Exception { } @Test - public void shouldGetStringArrayWhenParsingArrayNode() throws Exception { + public void shouldGetStringArrayWhenParsingArrayNode() { Map tree = new HashMap<>(); List subNodes = new ArrayList<>(); TextNode textNode1 = new TextNode("one"); @@ -119,86 +126,85 @@ public void shouldGetStringArrayWhenParsingArrayNode() throws Exception { 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(IsCollectionContaining.hasItems("one", "two"))); + assertThat(values, is(IsIterableContaining.hasItems("one", "two"))); } @Test - public void shouldGetStringArrayWhenParsingTextNode() throws Exception { + public void shouldGetStringArrayWhenParsingTextNode() { Map tree = new HashMap<>(); 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(IsCollectionContaining.hasItems("something"))); + assertThat(values, is(IsIterableContaining.hasItems("something"))); } @Test - public void shouldGetEmptyStringArrayWhenParsingEmptyTextNode() throws Exception { + public void shouldGetEmptyStringInArrayWhenParsingEmptyTextNode() { Map tree = new HashMap<>(); 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())); + assertThat(values, is(IsIterableContaining.hasItem(""))); } @Test - public void shouldGetNullArrayWhenParsingNullNode() throws Exception { + public void shouldGetNullArrayWhenParsingNullNode() { Map tree = new HashMap<>(); 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())); } @Test - public void shouldGetNullArrayWhenParsingNullNodeValue() throws Exception { + 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())); } @Test - public void shouldGetNullArrayWhenParsingNonArrayOrTextNode() throws Exception { + public void shouldGetNullArrayWhenParsingNonArrayOrTextNode() { Map tree = new HashMap<>(); 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())); } - @Test - public void shouldGetNullDateWhenParsingNullNode() throws Exception { + public void shouldGetNullInstantWhenParsingNullNode() { Map tree = new HashMap<>(); NullNode node = NullNode.getInstance(); tree.put("key", node); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(nullValue())); + Instant instant = deserializer.getInstantFromSeconds(tree, "key"); + assertThat(instant, is(nullValue())); } @Test - public void shouldGetNullDateWhenParsingNull() throws Exception { + public void shouldGetNullInstantWhenParsingNull() { Map tree = new HashMap<>(); tree.put("key", null); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(nullValue())); + Instant instant = deserializer.getInstantFromSeconds(tree, "key"); + assertThat(instant, is(nullValue())); } @Test - public void shouldThrowWhenParsingNonNumericNode() throws Exception { + public void shouldThrowWhenParsingNonNumericNode() { exception.expect(JWTDecodeException.class); exception.expectMessage("The claim 'key' contained a non-numeric date value."); @@ -206,36 +212,37 @@ public void shouldThrowWhenParsingNonNumericNode() throws Exception { TextNode node = new TextNode("123456789"); tree.put("key", node); - deserializer.getDateFromSeconds(tree, "key"); + deserializer.getInstantFromSeconds(tree, "key"); } @Test - public void shouldGetDateWhenParsingNumericNode() throws Exception { + public void shouldGetInstantWhenParsingNumericNode() { Map tree = new HashMap<>(); long seconds = 1478627949 / 1000; LongNode node = new LongNode(seconds); tree.put("key", node); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(notNullValue())); - assertThat(date.getTime(), is(seconds * 1000)); + Instant instant = deserializer.getInstantFromSeconds(tree, "key"); + assertThat(instant, is(notNullValue())); + assertThat(instant.toEpochMilli(), is(seconds * 1000)); } + @Test - public void shouldGetLargeDateWhenParsingNumericNode() throws Exception { + public void shouldGetLargeInstantWhenParsingNumericNode() { Map tree = new HashMap<>(); long seconds = Integer.MAX_VALUE + 10000L; LongNode node = new LongNode(seconds); tree.put("key", node); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(notNullValue())); - assertThat(date.getTime(), is(seconds * 1000)); - assertThat(date.getTime(), is(2147493647L * 1000)); + Instant instant = deserializer.getInstantFromSeconds(tree, "key"); + assertThat(instant, is(notNullValue())); + assertThat(instant.toEpochMilli(), is(seconds * 1000)); + assertThat(instant.toEpochMilli(), is(2147493647L * 1000)); } @Test - public void shouldGetNullStringWhenParsingNullNode() throws Exception { + public void shouldGetNullStringWhenParsingNullNode() { Map tree = new HashMap<>(); NullNode node = NullNode.getInstance(); tree.put("key", node); @@ -245,7 +252,7 @@ public void shouldGetNullStringWhenParsingNullNode() throws Exception { } @Test - public void shouldGetNullStringWhenParsingNull() throws Exception { + public void shouldGetNullStringWhenParsingNull() { Map tree = new HashMap<>(); tree.put("key", null); @@ -254,7 +261,7 @@ public void shouldGetNullStringWhenParsingNull() throws Exception { } @Test - public void shouldGetStringWhenParsingTextNode() throws Exception { + public void shouldGetStringWhenParsingTextNode() { Map tree = new HashMap<>(); TextNode node = new TextNode("something here"); tree.put("key", node); 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..5ad7ac68 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -6,17 +6,14 @@ import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.TextNode; import org.hamcrest.collection.IsCollectionWithSize; -import org.hamcrest.core.IsCollectionContaining; +import org.hamcrest.core.IsIterableContaining; import org.junit.Before; import org.junit.Rule; import org.junit.Test; 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.time.Instant; +import java.util.*; import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,148 +25,156 @@ public class PayloadImplTest { public ExpectedException exception = ExpectedException.none(); private PayloadImpl payload; - private Date expiresAt; - private Date notBefore; - private Date issuedAt; + private final Instant expiresAt = Instant.now().plusSeconds(10); + private final Instant notBefore = Instant.now(); + private final Instant issuedAt = Instant.now(); - private ObjectMapper mapper; - private ObjectReader objectReader; + private ObjectMapper objectMapper; @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); + public void setUp() { + 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); } - @SuppressWarnings("Convert2Diamond") @Test - public void shouldHaveUnmodifiableTree() throws Exception { + 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 shouldGetIssuer() throws Exception { + public void shouldHaveUnmodifiableAudience() { + exception.expect(UnsupportedOperationException.class); + PayloadImpl payload = new PayloadImpl(null, null, new ArrayList<>(), null, null, null, null, null, objectMapper); + payload.getAudience().add("something"); + } + + @Test + public void shouldGetIssuer() { assertThat(payload, is(notNullValue())); assertThat(payload.getIssuer(), is("issuer")); } @Test - public void shouldGetNullIssuerIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullIssuerIfMissing() { + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getIssuer(), is(nullValue())); } @Test - public void shouldGetSubject() throws Exception { + public void shouldGetSubject() { assertThat(payload, is(notNullValue())); assertThat(payload.getSubject(), is("subject")); } @Test - public void shouldGetNullSubjectIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullSubjectIfMissing() { + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getSubject(), is(nullValue())); } @Test - public void shouldGetAudience() throws Exception { + public void shouldGetAudience() { assertThat(payload, is(notNullValue())); assertThat(payload.getAudience(), is(IsCollectionWithSize.hasSize(1))); - assertThat(payload.getAudience(), is(IsCollectionContaining.hasItems("audience"))); + assertThat(payload.getAudience(), is(IsIterableContaining.hasItems("audience"))); } @Test - public void shouldGetNullAudienceIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullAudienceIfMissing() { + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getAudience(), is(nullValue())); } @Test - public void shouldGetExpiresAt() throws Exception { + public void shouldGetExpiresAt() { assertThat(payload, is(notNullValue())); - assertThat(payload.getExpiresAt(), is(expiresAt)); + assertThat(payload.getExpiresAt(), is(Date.from(expiresAt))); + assertThat(payload.getExpiresAtAsInstant(), is(expiresAt)); } @Test - public void shouldGetNullExpiresAtIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullExpiresAtIfMissing() { + 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())); } @Test - public void shouldGetNotBefore() throws Exception { + public void shouldGetNotBefore() { assertThat(payload, is(notNullValue())); - assertThat(payload.getNotBefore(), is(notBefore)); + assertThat(payload.getNotBefore(), is(Date.from(notBefore))); + assertThat(payload.getNotBeforeAsInstant(), is(notBefore)); } @Test - public void shouldGetNullNotBeforeIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullNotBeforeIfMissing() { + 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())); } @Test - public void shouldGetIssuedAt() throws Exception { + public void shouldGetIssuedAt() { assertThat(payload, is(notNullValue())); - assertThat(payload.getIssuedAt(), is(issuedAt)); + assertThat(payload.getIssuedAt(), is(Date.from(issuedAt))); + assertThat(payload.getIssuedAtAsInstant(), is(issuedAt)); } @Test - public void shouldGetNullIssuedAtIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullIssuedAtIfMissing() { + 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())); } @Test - public void shouldGetJWTId() throws Exception { + public void shouldGetJWTId() { assertThat(payload, is(notNullValue())); assertThat(payload.getId(), is("jwtId")); } @Test - public void shouldGetNullJWTIdIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNullJWTIdIfMissing() { + PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectMapper); assertThat(payload, is(notNullValue())); assertThat(payload.getId(), is(nullValue())); } @Test - public void shouldGetExtraClaim() throws Exception { + public void shouldGetExtraClaim() { assertThat(payload, is(notNullValue())); assertThat(payload.getClaim("extraClaim"), is(instanceOf(JsonNodeClaim.class))); assertThat(payload.getClaim("extraClaim").asString(), is("extraValue")); } @Test - public void shouldGetNotNullExtraClaimIfMissing() throws Exception { - PayloadImpl payload = new PayloadImpl(null, null, null, null, null, null, null, null, objectReader); + public void shouldGetNotNullExtraClaimIfMissing() { + 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"), is(instanceOf(NullClaim.class))); + assertThat(payload.getClaim("missing").isMissing(), is(true)); + assertThat(payload.getClaim("missing").isNull(), is(false)); } @Test - public void shouldGetClaims() throws Exception { + 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())); @@ -179,7 +184,7 @@ public void shouldGetClaims() throws Exception { } @Test - public void shouldNotAllowToModifyClaimsMap() throws Exception { + public void shouldNotAllowToModifyClaimsMap() { assertThat(payload, is(notNullValue())); Map claims = payload.getClaims(); assertThat(claims, is(notNullValue())); diff --git a/lib/src/test/java/com/auth0/jwt/impl/PayloadSerializerTest.java b/lib/src/test/java/com/auth0/jwt/impl/PayloadSerializerTest.java index ebdd5864..45761de0 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadSerializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadSerializerTest.java @@ -9,13 +9,10 @@ import org.junit.Test; import java.io.StringWriter; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; public class PayloadSerializerTest { @@ -34,10 +31,9 @@ public void setUp() throws Exception { serializerProvider = mapper.getSerializerProvider(); } - @SuppressWarnings("Convert2Diamond") @Test public void shouldSerializeEmptyMap() throws Exception { - ClaimsHolder holder = new ClaimsHolder(new HashMap()); + PayloadClaimsHolder holder = new PayloadClaimsHolder(new HashMap<>()); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -46,7 +42,7 @@ public void shouldSerializeEmptyMap() throws Exception { @Test public void shouldSerializeStringAudienceAsString() throws Exception { - ClaimsHolder holder = holderFor("aud", "auth0"); + PayloadClaimsHolder holder = holderFor("aud", "auth0"); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -55,7 +51,7 @@ public void shouldSerializeStringAudienceAsString() throws Exception { @Test public void shouldSerializeSingleItemAudienceAsArray() throws Exception { - ClaimsHolder holder = holderFor("aud", new String[]{"auth0"}); + PayloadClaimsHolder holder = holderFor("aud", new String[]{"auth0"}); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -64,7 +60,7 @@ public void shouldSerializeSingleItemAudienceAsArray() throws Exception { @Test public void shouldSerializeMultipleItemsAudienceAsArray() throws Exception { - ClaimsHolder holder = holderFor("aud", new String[]{"auth0", "auth10"}); + PayloadClaimsHolder holder = holderFor("aud", new String[]{"auth0", "auth10"}); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -73,7 +69,61 @@ public void shouldSerializeMultipleItemsAudienceAsArray() throws Exception { @Test public void shouldSkipSerializationOnEmptyAudience() throws Exception { - ClaimsHolder holder = holderFor("aud", new String[0]); + PayloadClaimsHolder holder = holderFor("aud", new String[0]); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{}"))); + } + + @Test + public void shouldSerializeSingleItemAudienceAsArrayWhenAList() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", Collections.singletonList("auth0")); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{\"aud\":\"auth0\"}"))); + } + + @Test + public void shouldSerializeMultipleItemsAudienceAsArrayWhenAList() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", Arrays.asList("auth0", "auth10")); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{\"aud\":[\"auth0\",\"auth10\"]}"))); + } + + @Test + public void shouldSkipSerializationOnEmptyAudienceWhenList() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", new ArrayList<>()); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{}"))); + } + + @Test + public void shouldSkipNonStringsOnAudienceWhenSingleItemList() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", Collections.singletonList(2)); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{}"))); + } + + @Test + public void shouldSkipNonStringsOnAudienceWhenList() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", Arrays.asList("auth0", 2, "auth10")); + serializer.serialize(holder, jsonGenerator, serializerProvider); + jsonGenerator.flush(); + + assertThat(writer.toString(), is(equalTo("{\"aud\":[\"auth0\",\"auth10\"]}"))); + } + + @Test + public void shouldSkipNonStringsOnAudience() throws Exception { + PayloadClaimsHolder holder = holderFor("aud", 4); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -82,7 +132,7 @@ public void shouldSkipSerializationOnEmptyAudience() throws Exception { @Test public void shouldSerializeNotBeforeDateInSeconds() throws Exception { - ClaimsHolder holder = holderFor("nbf", new Date(1478874000)); + PayloadClaimsHolder holder = holderFor("nbf", new Date(1478874000)); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -91,7 +141,7 @@ public void shouldSerializeNotBeforeDateInSeconds() throws Exception { @Test public void shouldSerializeIssuedAtDateInSeconds() throws Exception { - ClaimsHolder holder = holderFor("iat", new Date(1478874000)); + PayloadClaimsHolder holder = holderFor("iat", new Date(1478874000)); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -100,7 +150,7 @@ public void shouldSerializeIssuedAtDateInSeconds() throws Exception { @Test public void shouldSerializeExpiresAtDateInSeconds() throws Exception { - ClaimsHolder holder = holderFor("exp", new Date(1478874000)); + PayloadClaimsHolder holder = holderFor("exp", new Date(1478874000)); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -109,7 +159,7 @@ public void shouldSerializeExpiresAtDateInSeconds() throws Exception { @Test public void shouldSerializeCustomDateInSeconds() throws Exception { - ClaimsHolder holder = holderFor("birthdate", new Date(1478874000)); + PayloadClaimsHolder holder = holderFor("birthdate", new Date(1478874000)); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -120,25 +170,42 @@ public void shouldSerializeCustomDateInSeconds() throws Exception { public void shouldSerializeDatesUsingLong() throws Exception { long secs = Integer.MAX_VALUE + 10000L; Date date = new Date(secs * 1000L); - Map claims = new HashMap(); + Map claims = new HashMap<>(); claims.put("iat", date); claims.put("nbf", date); claims.put("exp", date); claims.put("ctm", date); - ClaimsHolder holder = new ClaimsHolder(claims); + claims.put("map", Collections.singletonMap("date", date)); + claims.put("list", Collections.singletonList(date)); + + Map nestedInMap = new HashMap<>(); + nestedInMap.put("list", Collections.singletonList(date)); + claims.put("nestedInMap", nestedInMap); + + List nestedInList = new ArrayList<>(); + nestedInList.add(Collections.singletonMap("nested", date)); + claims.put("nestedInList", nestedInList); + + PayloadClaimsHolder holder = new PayloadClaimsHolder(claims); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); String json = writer.toString(); + System.out.println(json); + assertThat(json, containsString("\"iat\":2147493647")); assertThat(json, containsString("\"nbf\":2147493647")); assertThat(json, containsString("\"exp\":2147493647")); assertThat(json, containsString("\"ctm\":2147493647")); + assertThat(json, containsString("\"map\":{\"date\":2147493647")); + assertThat(json, containsString("\"list\":[2147493647]")); + assertThat(json, containsString("\"nestedInMap\":{\"list\":[2147493647]}")); + assertThat(json, containsString("\"nestedInList\":[{\"nested\":2147493647}]")); } @Test public void shouldSerializeStrings() throws Exception { - ClaimsHolder holder = holderFor("name", "Auth0 Inc"); + PayloadClaimsHolder holder = holderFor("name", "Auth0 Inc"); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -147,7 +214,7 @@ public void shouldSerializeStrings() throws Exception { @Test public void shouldSerializeIntegers() throws Exception { - ClaimsHolder holder = holderFor("number", 12345); + PayloadClaimsHolder holder = holderFor("number", 12345); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -156,7 +223,7 @@ public void shouldSerializeIntegers() throws Exception { @Test public void shouldSerializeDoubles() throws Exception { - ClaimsHolder holder = holderFor("fraction", 23.45); + PayloadClaimsHolder holder = holderFor("fraction", 23.45); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -165,7 +232,7 @@ public void shouldSerializeDoubles() throws Exception { @Test public void shouldSerializeBooleans() throws Exception { - ClaimsHolder holder = holderFor("pro", true); + PayloadClaimsHolder holder = holderFor("pro", true); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -174,7 +241,7 @@ public void shouldSerializeBooleans() throws Exception { @Test public void shouldSerializeNulls() throws Exception { - ClaimsHolder holder = holderFor("id", null); + PayloadClaimsHolder holder = holderFor("id", null); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -185,7 +252,7 @@ public void shouldSerializeNulls() throws Exception { public void shouldSerializeCustomArrayOfObject() throws Exception { UserPojo user1 = new UserPojo("Michael", 1); UserPojo user2 = new UserPojo("Lucas", 2); - ClaimsHolder holder = holderFor("users", new UserPojo[]{user1, user2}); + PayloadClaimsHolder holder = holderFor("users", new UserPojo[]{user1, user2}); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -196,7 +263,7 @@ public void shouldSerializeCustomArrayOfObject() throws Exception { public void shouldSerializeCustomListOfObject() throws Exception { UserPojo user1 = new UserPojo("Michael", 1); UserPojo user2 = new UserPojo("Lucas", 2); - ClaimsHolder holder = holderFor("users", Arrays.asList(user1, user2)); + PayloadClaimsHolder holder = holderFor("users", Arrays.asList(user1, user2)); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); @@ -206,18 +273,17 @@ public void shouldSerializeCustomListOfObject() throws Exception { @Test public void shouldSerializeCustomObject() throws Exception { UserPojo user = new UserPojo("Michael", 1); - ClaimsHolder holder = holderFor("users", user); + PayloadClaimsHolder holder = holderFor("users", user); serializer.serialize(holder, jsonGenerator, serializerProvider); jsonGenerator.flush(); assertThat(writer.toString(), is(equalTo("{\"users\":{\"name\":\"Michael\",\"id\":1}}"))); } - @SuppressWarnings("Convert2Diamond") - private ClaimsHolder holderFor(String key, Object value) { - Map map = new HashMap(); + private PayloadClaimsHolder holderFor(String key, Object value) { + Map map = new HashMap<>(); map.put(key, value); - return new ClaimsHolder(map); + return new PayloadClaimsHolder(map); } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java new file mode 100644 index 00000000..61541ccf --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/interfaces/ClaimTest.java @@ -0,0 +1,98 @@ +package com.auth0.jwt.interfaces; + +import com.auth0.jwt.exceptions.JWTDecodeException; +import org.junit.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class ClaimTest { + + @Test + public void shouldGetInstantUsingDefault() { + Date date = new Date(1646667492000L); + Claim claim = new ClaimImplForTest(date); + assertThat(claim.asInstant(), is(date.toInstant())); + } + + @Test + public void shouldGetNullInstantUsingDefault() { + Claim claim = new ClaimImplForTest(null); + assertThat(claim.asInstant(), is(nullValue())); + } + + /** + * Implementation that does not override {@code asInstant()} + */ + static class ClaimImplForTest implements Claim { + private final Date date; + + ClaimImplForTest(Date date) { + this.date = date; + } + + @Override + public boolean isNull() { + return false; + } + + @Override + public boolean isMissing() { + return false; + } + + @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 date; + } + + @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 tClazz) throws JWTDecodeException { + return null; + } + } +} diff --git a/lib/src/test/java/com/auth0/jwt/interfaces/PayloadTest.java b/lib/src/test/java/com/auth0/jwt/interfaces/PayloadTest.java new file mode 100644 index 00000000..c5d2bd00 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/interfaces/PayloadTest.java @@ -0,0 +1,83 @@ +package com.auth0.jwt.interfaces; + +import org.junit.Test; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class PayloadTest { + + @Test + public void shouldGetInstantFromDefault() { + Date date = new Date(1646667492000L); + Payload payload = new PayloadImplForTest(date); + assertThat(payload.getExpiresAtAsInstant(), is(date.toInstant())); + assertThat(payload.getIssuedAtAsInstant(), is(date.toInstant())); + assertThat(payload.getNotBeforeAsInstant(), is(date.toInstant())); + } + + @Test + public void shouldGetInstantFromDefaultAsNu() { + Payload payload = new PayloadImplForTest(null); + assertThat(payload.getExpiresAtAsInstant(), is(nullValue())); + assertThat(payload.getIssuedAtAsInstant(), is(nullValue())); + assertThat(payload.getNotBeforeAsInstant(), is(nullValue())); + } + + static class PayloadImplForTest implements Payload { + private final Date date; + + PayloadImplForTest(Date date) { + this.date = date; + } + + @Override + public String getIssuer() { + return null; + } + + @Override + public String getSubject() { + return null; + } + + @Override + public List getAudience() { + return null; + } + + @Override + public Date getExpiresAt() { + return date; + } + + @Override + public Date getNotBefore() { + return date; + } + + @Override + public Date getIssuedAt() { + return date; + } + + @Override + public String getId() { + return null; + } + + @Override + public Claim getClaim(String name) { + return null; + } + + @Override + public Map getClaims() { + 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 new file mode 100644 index 00000000..11acb029 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/interfaces/VerificationTest.java @@ -0,0 +1,171 @@ +package com.auth0.jwt.interfaces; + +import com.auth0.jwt.JWTVerifier; +import org.junit.Test; + +import java.time.Instant; +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; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThrows; + +/** + * Tests for any default method implementations in the {@link Verification} interface. + */ +public class VerificationTest { + + @Test + public void withInstantClaimShouldUseDefaultImpl() { + Instant instant = Instant.ofEpochSecond(1478891521); + Verification verification = new VerificationImplForTest() + .withClaim("name", instant); + + assertThat(verification, instanceOf(VerificationImplForTest.class)); + assertThat(((VerificationImplForTest)verification).expectedClaims, hasEntry("name", Date.from(instant))); + } + + @Test + public void withInstantClaimShouldUseDefaultImplAndHandleNull() { + Verification verification = new VerificationImplForTest() + .withClaim("name", (Instant) null); + + assertThat(verification, instanceOf(VerificationImplForTest.class)); + assertThat(((VerificationImplForTest)verification).expectedClaims, hasEntry("name", null)); + } + + @Test + public void withIssuerStringDefaultImplShouldDelegate() { + Verification verification = new VerificationImplForTest() + .withIssuer("string"); + + assertThat(verification, instanceOf(VerificationImplForTest.class)); + assertThat(((VerificationImplForTest)verification).expectedClaims, hasEntry("iss", new String[]{"string"})); + } + + static class VerificationImplForTest implements Verification { + + private final Map expectedClaims = new HashMap<>(); + + @Override + public Verification withIssuer(String... issuer) { + expectedClaims.put("iss", issuer); + return this; + } + + @Override + public Verification withSubject(String subject) { + return null; + } + + @Override + 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; + } + + @Override + public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { + return null; + } + + @Override + public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { + return null; + } + + @Override + public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withJWTId(String jwtId) { + return null; + } + + @Override + public Verification withClaimPresence(String name) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withNullClaim(String name) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, Integer value) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, Long value) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, Double value) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, String value) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, Date value) throws IllegalArgumentException { + this.expectedClaims.put(name, value); + return this; + } + + @Override + public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withArrayClaim(String name, Long... items) throws IllegalArgumentException { + return null; + } + + @Override + public Verification withClaim(String name, BiPredicate predicate) throws IllegalArgumentException { + return null; + } + + @Override + public Verification ignoreIssuedAt() { + return null; + } + + @Override + public JWTVerifier build() { + return null; + } + } +} diff --git a/local.properties b/local.properties deleted file mode 100644 index 93e073d4..00000000 --- a/local.properties +++ /dev/null @@ -1,12 +0,0 @@ -## This file is automatically generated by Android Studio. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must *NOT* be checked into Version Control Systems, -# as it contains information specific to your local configuration. -# -# Location of the SDK. This is only used by Gradle. -# For customization when using a Version Control System, please read the -# header note. -#Fri Dec 02 18:18:12 ART 2016 -ndk.dir=/Users/hernan/Development/Android.SDK/ndk-bundle -sdk.dir=/Users/hernan/Development/Android.SDK 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: diff --git a/settings.gradle b/settings.gradle index ce7f0a64..d3c4c85b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,8 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} + 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')