diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 909f794c..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: 2 -jobs: - java_7_build: - docker: - - image: openjdk:7u121-jdk - steps: - - checkout - - run: chmod +x gradlew - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "build.gradle" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - # run tests! - - run: ./gradlew clean check jacocoTestReport --continue --console=plain - - run: - name: Upload Coverage - when: on_success - command: bash <(curl -s https://codecov.io/bash) - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb - - java_8_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) - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "build.gradle" }} - environment: - GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"' - _JAVA_OPTIONS: "-Xms512m -Xmx1024m" - TERM: dumb - -workflows: - version: 2 - build: - jobs: - - java_7_build - - java_8_build diff --git a/.codecov.yml b/.codecov.yml index 63e5785f..de278af2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,10 +5,12 @@ coverage: status: patch: default: - if_no_uploads: error + if_no_uploads: success + if_ci_failed: error changes: true 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 new file mode 100644 index 00000000..7958e8bd --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @auth0/project-dx-sdks-engineer-codeowner 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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..8675fc7e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +### Changes + +Please describe both what is changing and why this is important. Include: + +- Endpoints added, deleted, deprecated, or changed +- Classes and methods added, deleted, deprecated, or changed +- Screenshots of new or changed UI, if applicable +- A summary of usage if this is a new feature or change to a public API (this should also be added to relevant documentation once released) +- Any alternative designs or approaches considered + +### References + +Please include relevant links supporting this change such as a: + +- support ticket +- community post +- StackOverflow post +- support forum thread + +### Testing + +Please describe how this can be tested by reviewers. Be specific about anything not tested and reasons why. If this library has unit and/or integration testing, tests should be added for new functionality and existing tests should complete without errors. + +- [ ] This change adds test coverage +- [ ] This change has been tested on the latest version of Java or why not + +### Checklist + +- [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) +- [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) +- [ ] All existing and new tests complete without errors 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/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..b2e13fc7 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,20 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 90 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +daysUntilClose: 7 + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: [] + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: true + +# Label to use when marking as stale +staleLabel: closed:stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ \ No newline at end of file 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 ab35b855..e706fbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Temporary Items ## Plugin-specific files: # IntelliJ +bin/ /out/ /lib/out/ @@ -56,6 +57,11 @@ crashlytics.properties crashlytics-build.properties fabric.properties +# Eclipse IDE +.classpath +.project +.settings/ + ### Java template *.class @@ -75,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 cfb8210b..b97fab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,411 @@ # 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) + +**Fixed** +- Fixed an NPE on null map and list claims [\#417](https://github.com/auth0/java-jwt/pull/417) ([Vorotyntsev](https://github.com/Vorotyntsev)) + +## [3.10.2](https://github.com/auth0/java-jwt/tree/3.10.2) (2020-03-27) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.1...3.10.2) + +**Fixed** +- JavaDoc fix [\#413](https://github.com/auth0/java-jwt/pull/413) ([jimmyjames](https://github.com/jimmyjames)) +- Check varargs null values in JWTVerifier [\#412](https://github.com/auth0/java-jwt/pull/412) ([jimmyjames](https://github.com/jimmyjames)) + +## [3.10.1](https://github.com/auth0/java-jwt/tree/3.10.1) (2020-03-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.10.0...3.10.1) + +**Changed** +- Update Jackson and Commons Codec dependencies [\#407](https://github.com/auth0/java-jwt/pull/407) ([jimmyjames](https://github.com/jimmyjames)) + +**Security** +- Update jackson-databind to 2.10.2 [\#399](https://github.com/auth0/java-jwt/pull/399) ([gexclaude](https://github.com/gexclaude)) + +## [3.10.0](https://github.com/auth0/java-jwt/tree/3.10.0) (2020-02-14) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.9.0...3.10.0) +**Closed issues** +- NullPointerException when the claim doesn't exist in the token [\#384](https://github.com/auth0/java-jwt/issues/384) + +**Added** +- Add Javadoc URL and badge to the README [\#382](https://github.com/auth0/java-jwt/pull/382) ([lbalmaceda](https://github.com/lbalmaceda)) +- Allow to customize the typ header claim [\#381](https://github.com/auth0/java-jwt/pull/381) ([lbalmaceda](https://github.com/lbalmaceda)) +- JWTCreator for basic types [\#282](https://github.com/auth0/java-jwt/pull/282) ([skjolber](https://github.com/skjolber)) +- Support verification of Long[] datatype like in JWTCreator [\#278](https://github.com/auth0/java-jwt/pull/278) ([skjolber](https://github.com/skjolber)) + +**Changed** +- Update to Gradle 6.1.1 [\#389](https://github.com/auth0/java-jwt/pull/389) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Handle missing expected array claim [\#393](https://github.com/auth0/java-jwt/pull/393) ([lbalmaceda](https://github.com/lbalmaceda)) +- Update tests to use valid Base64 URL-encoded tokens [\#386](https://github.com/auth0/java-jwt/pull/386) ([jimmyjames](https://github.com/jimmyjames)) + +## [3.9.0](https://github.com/auth0/java-jwt/tree/3.9.0) (2020-01-02) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.3...3.9.0) + +**Added** +- Support serialization of DecodedJWT [\#370](https://github.com/auth0/java-jwt/pull/370) ([jimmyjames](https://github.com/jimmyjames)) + +**Fixed** +- Fixing JwtCreator builder when setting headers as a map [\#320](https://github.com/auth0/java-jwt/pull/320) ([maxbalan](https://github.com/maxbalan)) + +## [3.8.3](https://github.com/auth0/java-jwt/tree/3.8.3) (2019-09-25) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.2...3.8.3) + +**Security** +- Fix: updated jackson-databind to 2.10.0.pr3 to block CVE [\#356](https://github.com/auth0/java-jwt/pull/356) ([danbrodsky](https://github.com/danbrodsky)) + +## [3.8.2](https://github.com/auth0/java-jwt/tree/3.8.2) (2019-08-15) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.1...3.8.2) + +**Security** +- Fix: updated jackson-databind to 2.9.9.3 to block CVE [\#347](https://github.com/auth0/java-jwt/pull/347) ([danbrodsky](https://github.com/danbrodsky)) + +## [3.8.1](https://github.com/auth0/java-jwt/tree/3.8.1) (2019-05-22) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.8.0...3.8.1) + +**Security** +- Bump dependencies and fix security issue [\#337](https://github.com/auth0/java-jwt/pull/337) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.8.0](https://github.com/auth0/java-jwt/tree/3.8.0) (2019-03-14) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.7.0...3.8.0) + +**Added** +- Support multiple issuers #246 [\#288](https://github.com/auth0/java-jwt/pull/288) ([itdevelopmentapps](https://github.com/itdevelopmentapps)) + +## [3.7.0](https://github.com/auth0/java-jwt/tree/3.7.0) (2019-01-29) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.6.0...3.7.0) + +**Added** +- Performance improvements [\#255](https://github.com/auth0/java-jwt/pull/255) ([skjolber](https://github.com/skjolber)) + +## [3.6.0](https://github.com/auth0/java-jwt/tree/3.6.0) (2019-01-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.5.0...3.6.0) + +**Added** +- Allow to skip "issued at" validation [\#297](https://github.com/auth0/java-jwt/pull/297) ([complanboy2](https://github.com/complanboy2)) + +## [3.5.0](https://github.com/auth0/java-jwt/tree/3.5.0) (2019-01-03) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.1...3.5.0) + +**Added** +- Verify a DecodedJWT [\#308](https://github.com/auth0/java-jwt/pull/308) ([martinoconnor](https://github.com/martinoconnor)) + +**Changed** +- Add an interface for JWTVerifier. [\#205](https://github.com/auth0/java-jwt/pull/205) ([jebbench](https://github.com/jebbench)) + +**Fixed** +- Remove unnecessary cast between long/double and floor call [\#296](https://github.com/auth0/java-jwt/pull/296) ([jhorstmann](https://github.com/jhorstmann)) + +**Security** +- Bump jackson-databind to patch security issues [\#309](https://github.com/auth0/java-jwt/pull/309) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.4.1](https://github.com/auth0/java-jwt/tree/3.4.1) (2018-10-24) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.4.0...3.4.1) + +**Security** +- Update jackson-databind dependency [\#292](https://github.com/auth0/java-jwt/pull/292) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.4.0](https://github.com/auth0/java-jwt/tree/3.4.0) (2018-06-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.3.0...3.4.0) + +**Breaking Changes** +- Fix for [\#236](https://github.com/auth0/java-jwt/pull/236) - refactored HMACAlgorithm so that it doesn't throw an UnsupportedEncodingException [\#242](https://github.com/auth0/java-jwt/pull/242) ([obecker](https://github.com/obecker)). + +Clients using the following methods may need to update their code to not catch an `UnsupportedEncodingException`: +- `public static Algorithm HMAC384(String secret)` +- `public static Algorithm HMAC256(String secret)` +- `public static Algorithm HMAC512(String secret)` + +**Changed** +- Throw JWTDecodeException when date claim format is invalid [\#241](https://github.com/auth0/java-jwt/pull/241) ([lbalmaceda](https://github.com/lbalmaceda)) + +**Security** +- Bump Jackson dependency [\#244](https://github.com/auth0/java-jwt/pull/244) ([skjolber](https://github.com/skjolber)) + ## [3.3.0](https://github.com/auth0/java-jwt/tree/3.3.0) (2017-11-06) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.2.0...3.3.0) **Closed issues** diff --git a/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 4ce86422..9d0ae41c 100644 --- a/README.md +++ b/README.md @@ -1,36 +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 +![Build Status](https://img.shields.io/github/checks-status/auth0/java-jwt/master) +[![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) +[![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org/) +[![Maven Central](https://img.shields.io/maven-central/v/com.auth0/java-jwt.svg?style=flat-square)](https://mvnrepository.com/artifact/com.auth0/java-jwt) +[![javadoc](https://javadoc.io/badge2/com.auth0/auth0/javadoc.svg)](https://javadoc.io/doc/com.auth0/java-jwt) -[![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) -[![Coverage Status](https://img.shields.io/codecov/c/github/auth0/java-jwt/v3.svg?style=flat-square)](https://codecov.io/github/auth0/java-jwt) -[![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://doge.mit-license.org) +:books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :computer: [API Reference](#api-reference) :speech_balloon: [Feedback](#feedback) -A Java implementation of [JSON Web Tokens (draft-ietf-oauth-json-web-token-08)](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html). +## 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. -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. +## Getting Started -## Installation +### Requirements -### Maven +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. -```xml - - com.auth0 - java-jwt - 3.3.0 - -``` +> `java-jwt` is intended for server-side JVM applications. Android applications should use [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android). -### Gradle - -```gradle -compile 'com.auth0:java-jwt:3.3.0' -``` - -## 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 | | :-------------: | :-------------: | :----- | @@ -44,395 +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 + ``` -#### Using a KeyProvider: - -By using a `KeyProvider` you can change in runtime the key used either to verify the token signature or to sign a new token for RSA or ECDSA algorithms. This is achieved by implementing either `RSAKeyProvider` or `ECDSAKeyProvider` methods: - -- `getPublicKeyById(String kid)`: Its called during token signature verification and it should return the key used to verify the token. If key rotation is being used, e.g. [JWK](https://tools.ietf.org/html/rfc7517) it can fetch the correct rotation key using the id. (Or just return the same key all the time). -- `getPrivateKey()`: Its called during token signing and it should return the key that will be used to sign the JWT. -- `getPrivateKeyId()`: Its called during token signing and it should return the id of the key that identifies the one returned by `getPrivateKey()`. This value is preferred over the one set in the `JWTCreator.Builder#withKeyId(String)` method. If you don't need to set a `kid` value avoid instantiating an Algorithm using a `KeyProvider`. - - -The following snippet uses example classes showing how this would work: - +or Gradle: -```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' ``` -> For simple key rotation using JWKs try the [jwks-rsa-java](https://github.com/auth0/jwks-rsa-java) library. - +### Create a JWT -### Create and Sign a Token +Use `JWT.create()`, configure the claims, and then call `sign(algorithm)` to sign the 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. - -* 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 (UnsupportedEncodingException exception){ - //UTF-8 encoding not supported } 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. +Create a `JWTVerifier` passing the `Algorithm`, and specify any required claim values. -* Example using `HS256` +The following example uses `RS256` to verify the JWT. ```java String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; +DecodedJWT decodedJWT; try { - Algorithm algorithm = Algorithm.HMAC256("secret"); + 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); -} catch (UnsupportedEncodingException exception){ - //UTF-8 encoding not supported + // reusable verifier instance + .build(); + + decodedJWT = verifier.verify(token); } catch (JWTVerificationException exception){ - //Invalid signature/claims + // Invalid signature/claims } ``` -* Example using `RS256` - -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -RSAPublicKey publicKey = //Get the key instance -RSAPrivateKey privateKey = //Get the key instance -try { - Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); - JWTVerifier verifier = JWT.require(algorithm) - .withIssuer("auth0") - .build(); //Reusable verifier instance - DecodedJWT jwt = verifier.verify(token); -} catch (JWTVerificationException exception){ - //Invalid signature/claims -} -``` - -If the token has an invalid signature or the Claim requirement is not met, a `JWTVerificationException` will raise. - - -#### 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 -} -``` - -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 b18f7596..c938b200 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - jcenter() - } - dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7' - } -} - 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 13372aef..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 04e285f3..ec991f9a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Dec 28 10:00:20 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradlew b/gradlew index 9d82f789..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,28 +75,14 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -85,7 +106,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -105,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 @@ -134,27 +156,30 @@ 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 -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@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 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, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem 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" + @rem Find java.exe 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. @@ -35,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% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line 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 fb782d71..83093fc1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,46 +1,143 @@ -apply plugin: 'jacoco' -apply plugin: 'java' -apply from: '../scripts/release.gradle' -apply from: '../scripts/maven.gradle' -apply from: '../scripts/bintray.gradle' +buildscript { + repositories { + jcenter() + } + + dependencies { + // https://github.com/melix/japicmp-gradle-plugin/issues/36 + classpath 'com.google.guava:guava:31.1-jre' + } +} + +plugins { + id 'java' + id 'jacoco' + id 'checkstyle' + id 'me.champeau.gradle.japicmp' version '0.4.1' +} + +sourceSets { + jmh { + + } +} + +configurations { + jmhImplementation { + extendsFrom implementation + } +} + +checkstyle { + toolVersion '10.0' +} +//We are disabling lint checks for tests +tasks.named("checkstyleTest").configure({ + enabled = false +}) +tasks.named("checkstyleJmh").configure({ + enabled = false +}) -logger.lifecycle("Using version ${version} for ${group}.${name}") +apply from: rootProject.file('gradle/versioning.gradle') -auth0 { - name "java jwt" - repo "java-jwt" - description "Java implementation of JSON Web Token (JWT)" - url 'http://www.jwt.io' - developer { - id = "auth0" - name = "Auth0" - email = "oss@auth0.com" +version = getVersionFromFile() +group = GROUP +logger.lifecycle("Using version ${version} for ${name} group $group") + +import me.champeau.gradle.japicmp.JapicmpTask + +project.afterEvaluate { + + 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')) + } + tasks.named('check') { + dependsOn(taskName) + } } - developer { - id = "lbalmaceda" - name = "Luciano Balmaceda" - email = "luciano.balmaceda@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}") + } + } } - developer { - id = "hzalaz" - name = "Hernan Zalazar" - email = "hernan@auth0.com" +} + +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 { - compile 'com.fasterxml.jackson.core:jackson-databind:2.9.2' - compile 'commons-codec:commons-codec:1.11' - testCompile 'org.bouncycastle:bcprov-jdk15on:1.58' - testCompile 'junit:junit:4.12' - testCompile 'net.jodah:concurrentunit:0.4.3' - testCompile 'org.hamcrest:java-hamcrest:2.0.0.0' - testCompile 'org.mockito:mockito-core:2.11.0' + 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 { @@ -57,7 +154,91 @@ test { } } -task clean(type: Delete) { - delete rootProject.buildDir - delete 'CHANGELOG.md.release' +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 f8e5cfc3..696abe40 100644 --- a/lib/src/main/java/com/auth0/jwt/JWT.java +++ b/lib/src/main/java/com/auth0/jwt/JWT.java @@ -2,30 +2,61 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.impl.JWTParser; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.Verification; +/** + * Exposes all the JWT functionalities. + */ @SuppressWarnings("WeakerAccess") -public abstract class JWT { +public class JWT { + + private final JWTParser parser; + + /** + * Constructs a new instance of the JWT library. Use this if you need to decode many JWT + * tokens on the fly and do not wish to instantiate a new parser for each invocation. + */ + public JWT() { + parser = new JWTParser(); + } + + /** + * Decode a given Json Web Token. + *

+ * Note that this method doesn't verify the token's signature! + * Use it only if you trust the token or 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. + */ + public DecodedJWT decodeJwt(String token) throws JWTDecodeException { + return new JWTDecoder(parser, token); + } /** * 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) { @@ -33,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 a0889876..bfcb9147 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -3,22 +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.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 { @@ -27,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); } @@ -57,33 +68,67 @@ 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 * * @param headerClaims the values to use as Claims in the token's Header. * @return this same Builder instance. */ public Builder withHeader(Map headerClaims) { - this.headerClaims = new HashMap<>(headerClaims); + if (headerClaims == null) { + return this; + } + + for (Map.Entry entry : headerClaims.entrySet()) { + if (entry.getValue() == null) { + this.headerClaims.remove(entry.getKey()); + } else { + this.headerClaims.put(entry.getKey(), entry.getValue()); + } + } + return this; } + /** + * 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; } @@ -94,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; } @@ -105,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; } @@ -116,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 Issued At ("iat") 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(Instant notBefore) { + addClaim(RegisteredClaims.NOT_BEFORE, notBefore); + 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(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; } @@ -160,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; } @@ -235,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. @@ -248,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. * @@ -282,7 +443,7 @@ public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgume * @param name the Claim's name. * @param items the Claim's value. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null. + * @throws IllegalArgumentException if the name is null */ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { assertNonNull(name); @@ -291,19 +452,152 @@ public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentE } /** - * Creates a new JWT and signs is with the given algorithm + * Add specific Claims to set as the Payload. If the provided map is null then + * nothing is changed. + *

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

+ * + *

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

+ * + * @param payloadClaims the values to use as Claims in the token's payload. + * @return this same Builder instance. + * @throws IllegalArgumentException if any of the claim keys or null, + * or if the values are not of a supported type. + */ + public Builder withPayload(Map payloadClaims) throws IllegalArgumentException { + if (payloadClaims == null) { + return this; + } + + 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 specific Claims to set as the Payload. If the provided json is null then + * nothing is changed. + * + *

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

+ * + * @param payloadClaimsJson the values to use as Claims in the token's payload. + * @return this same Builder instance. + * @throws IllegalArgumentException if any of the claim keys or null, + * or if the values are not of a supported type, + * or if json value has invalid structure. + */ + public Builder withPayload(String payloadClaimsJson) throws IllegalArgumentException { + if (payloadClaimsJson == null) { + return this; + } + + try { + Map payloadClaims = mapper.readValue(payloadClaimsJson, LinkedHashMap.class); + return withPayload(payloadClaims); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid payload JSON", e); + } + } + + private boolean validatePayload(Map payload) { + for (Map.Entry entry : payload.entrySet()) { + String key = entry.getKey(); + 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 (!isSupportedType(value)) { + return false; + } + + if (!(entry.getKey() instanceof String)) { + return false; + } + } + return true; + } + + private static boolean validateClaim(List list) { + // accept null values in list + for (Object object : list) { + if (!isSupportedType(object)) { + return false; + } + } + return true; + } + + private static boolean isSupportedType(Object value) { + if (value instanceof List) { + return validateClaim((List) value); + } else if (value instanceof Map) { + return validateClaim((Map) value); + } else { + return isBasicType(value); + } + } + + private static boolean isBasicType(Object value) { + if (value == null) { + return true; + } else { + Class c = value.getClass(); + + if (c.isArray()) { + return c == Integer[].class || c == Long[].class || c == String[].class; + } + return c == String.class || c == Integer.class || c == Long.class || c == Double.class + || c == Date.class || c == Instant.class || c == Boolean.class; + } + } + + /** + * 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()); - 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) { withKeyId(signingKeyId); @@ -318,22 +612,20 @@ 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)); - String content = String.format("%s.%s", header, payload); + 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(content.getBytes(StandardCharsets.UTF_8)); - String signature = Base64.encodeBase64URLSafeString((signatureBytes)); + 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", content, signature); + return String.format("%s.%s.%s", header, payload, signature); } } diff --git a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java index 7921c128..cc283095 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -6,33 +6,44 @@ 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 { +final class JWTDecoder implements DecodedJWT, Serializable { + + private static final long serialVersionUID = 1873362438023312895L; private final String[] parts; private final Header header; private final Payload payload; JWTDecoder(String jwt) throws JWTDecodeException { + this(new JWTParser(), jwt); + } + + JWTDecoder(JWTParser converter, String jwt) throws JWTDecodeException { parts = TokenUtils.splitToken(jwt); - final JWTParser converter = new JWTParser(); String headerJson; String payloadJson; try { - 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); @@ -83,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 6f513c7e..bf180300 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -2,34 +2,43 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.*; -import com.auth0.jwt.impl.PublicClaims; +import com.auth0.jwt.impl.JWTParser; 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 { +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 { @@ -37,12 +46,19 @@ static Verification init(Algorithm algorithm) throws IllegalArgumentException { } /** - * The Verification class holds the Claims required by a JWT to be valid. + * {@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) { @@ -50,54 +66,66 @@ public static class BaseVerification implements Verification { } this.algorithm = algorithm; - this.claims = new HashMap<>(); + this.expectedChecks = new ArrayList<>(); + this.customLeeways = new HashMap<>(); this.defaultLeeway = 0; } - /** - * Require a specific Issuer ("iss") claim. - * - * @param issuer the required Issuer value - * @return this same Verification instance. - */ @Override - public Verification withIssuer(String issuer) { - requireClaim(PublicClaims.ISSUER, issuer); + public Verification withIssuer(String... 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; } - /** - * Require a specific Subject ("sub") claim. - * - * @param subject the required Subject value - * @return this same Verification instance. - */ @Override public Verification withSubject(String subject) { - requireClaim(PublicClaims.SUBJECT, subject); + addCheck(RegisteredClaims.SUBJECT, (claim, decodedJWT) -> + verifyNull(claim, subject) || subject.equals(claim.asString())); return this; } - /** - * Require a specific Audience ("aud") claim. - * - * @param audience the required Audience value - * @return this same Verification instance. - */ @Override public Verification withAudience(String... audience) { - requireClaim(PublicClaims.AUDIENCE, 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; } - /** - * Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - * Setting a specific leeway value on a given Claim will override this value for that Claim. - * - * @param leeway the window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptLeeway(long leeway) throws IllegalArgumentException { assertPositive(leeway); @@ -105,191 +133,147 @@ public Verification acceptLeeway(long leeway) throws IllegalArgumentException { return this; } - /** - * Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. - * Expiration Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Expires At Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.EXPIRES_AT, leeway); + customLeeways.put(RegisteredClaims.EXPIRES_AT, leeway); return this; } - /** - * Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. - * Not Before Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Not Before Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptNotBefore(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.NOT_BEFORE, leeway); + customLeeways.put(RegisteredClaims.NOT_BEFORE, leeway); return this; } - /** - * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. - * Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway - * - * @param leeway the window in seconds in which the Issued At Claim will still be valid. - * @return this same Verification instance. - * @throws IllegalArgumentException if leeway is negative. - */ @Override public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException { assertPositive(leeway); - requireClaim(PublicClaims.ISSUED_AT, leeway); + customLeeways.put(RegisteredClaims.ISSUED_AT, leeway); + return this; + } + + @Override + public Verification ignoreIssuedAt() { + this.ignoreIssuedAt = true; return this; } - /** - * Require a specific JWT Id ("jti") claim. - * - * @param jwtId the required Id value - * @return this same Verification instance. - */ @Override public Verification withJWTId(String jwtId) { - requireClaim(PublicClaims.JWT_ID, jwtId); + 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; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asBoolean()))); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Integer value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asInt()))); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Long value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asLong()))); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Double value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asDouble()))); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, String value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asString()))); return this; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name. - * @param value the Claim's value. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withClaim(String name, Date value) throws IllegalArgumentException { + return withClaim(name, value != null ? value.toInstant() : null); + } + + @Override + public Verification withClaim(String name, Instant value) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, value); + // 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); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, predicate) + || predicate.test(claim, decodedJWT))); return this; } - /** - * Require a specific Array Claim to contain at least the given items. - * - * @param name the Claim's name. - * @param items the items the Claim must contain. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { assertNonNull(name); - requireClaim(name, items); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } - /** - * Require a specific Array Claim to contain at least the given items. - * - * @param name the Claim's name. - * @param items the items the Claim must contain. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null. - */ @Override public Verification withArrayClaim(String name, 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); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); return this; } - /** - * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. - * - * @return a new JWTVerifier instance. - */ @Override public JWTVerifier build() { - return this.build(new ClockImpl()); + return this.build(Clock.systemUTC()); } /** @@ -297,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) { @@ -316,24 +391,45 @@ private void assertNonNull(String name) { } } - private void addLeewayToDateClaims() { - if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { - claims.put(PublicClaims.EXPIRES_AT, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { - claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); - } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); - } + private void addCheck(String name, BiPredicate predicate) { + expectedChecks.add(constructExpectedCheck(name, (claim, decodedJWT) -> { + if (claim.isMissing()) { + throw new MissingClaimException(name); + } + return predicate.test(claim, decodedJWT); + })); + } + + private ExpectedCheckHolder constructExpectedCheck(String claimName, BiPredicate check) { + return new ExpectedCheckHolder() { + @Override + public String getClaimName() { + return claimName; + } + + @Override + public boolean verify(Claim claim, DecodedJWT decodedJWT) { + return check.test(claim, decodedJWT); + } + }; + } + + private boolean verifyNull(Claim claim, Object value) { + return value == null && claim.isNull(); } - private void requireClaim(String name, Object value) { - if (value == null) { - claims.remove(name); - return; + private boolean isNullOrEmpty(String[] args) { + if (args == null || args.length == 0) { + return true; } - claims.put(name, value); + boolean isAllNull = true; + for (String arg : args) { + if (arg != null) { + isAllNull = false; + break; + } + } + return isAllNull; } } @@ -343,115 +439,62 @@ private void requireClaim(String name, Object value) { * * @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 { - DecodedJWT jwt = JWT.decode(token); + DecodedJWT jwt = new JWTDecoder(parser, token); + return verify(jwt); + } + + /** + * Perform the verification against the given decoded JWT, using any previous configured options. + * + * @param jwt to verify. + * @return a verified and decoded JWT. + * @throws AlgorithmMismatchException if the algorithm stated in the token's header 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 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: - //noinspection unchecked - 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: - assertValidStringClaim(entry.getKey(), jwt.getIssuer(), (String) 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 = 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((long) Math.floor((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."); - } - } } 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 12080a02..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.UnsupportedEncodingException; 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,65 +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 UnsupportedEncodingException if the current Java platform implementation doesn't support the UTF-8 character encoding. + * @throws IllegalArgumentException if the provided Secret is null. */ - public static Algorithm HMAC256(String secret) throws IllegalArgumentException, UnsupportedEncodingException { + 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. - * @throws UnsupportedEncodingException if the current Java platform implementation doesn't support the UTF-8 character encoding. + * @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, UnsupportedEncodingException { - 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. - * @throws UnsupportedEncodingException if the current Java platform implementation doesn't support the UTF-8 character encoding. + * @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, UnsupportedEncodingException { - 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. */ @@ -201,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". * @@ -230,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; @@ -268,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; @@ -306,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; @@ -326,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. */ @@ -344,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. */ @@ -361,16 +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 contentBytes an array of bytes representing the base64 encoded 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); + } + + /** + * 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. + * @return the signature in a base64 encoded array of bytes + * @throws SignatureGenerationException if the Key is invalid. + */ + public abstract byte[] sign(byte[] contentBytes) throws SignatureGenerationException; + } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java b/lib/src/main/java/com/auth0/jwt/algorithms/CryptoHelper.java index 08af662c..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,28 +2,204 @@ 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 { - boolean verifySignatureFor(String algorithm, byte[] secretBytes, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException { - return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, contentBytes), signatureBytes); + private static final byte JWT_PART_SEPARATOR = (byte) 46; + + /** + * Verify signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param secretBytes algorithm secret. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + + boolean verifySignatureFor( + String algorithm, + byte[] secretBytes, + String header, + String payload, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException { + return verifySignatureFor(algorithm, secretBytes, + header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8), signatureBytes); } - 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 for JWT header and 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. + */ + + boolean verifySignatureFor( + String algorithm, + byte[] secretBytes, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException { + return MessageDigest.isEqual(createSignatureFor(algorithm, secretBytes, headerBytes, payloadBytes), + signatureBytes); } - boolean verifySignatureFor(String algorithm, PublicKey publicKey, byte[] contentBytes, byte[] signatureBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + /** + * Verify signature for JWT header and payload. + * + * @param algorithm algorithm name. + * @param publicKey algorithm public key. + * @param header JWT header. + * @param payload JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + String header, + String payload, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + return verifySignatureFor(algorithm, publicKey, header.getBytes(StandardCharsets.UTF_8), + payload.getBytes(StandardCharsets.UTF_8), signatureBytes); + } + + /** + * Verify signature for JWT header and payload using a public key. + * + * @param algorithm algorithm name. + * @param publicKey the public key to use for verification. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @param signatureBytes JWT signature. + * @return true if signature is valid. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + */ + boolean verifySignatureFor( + String algorithm, + PublicKey publicKey, + byte[] headerBytes, + byte[] payloadBytes, + byte[] signatureBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { final Signature s = Signature.getInstance(algorithm); s.initVerify(publicKey); - s.update(contentBytes); + s.update(headerBytes); + s.update(JWT_PART_SEPARATOR); + s.update(payloadBytes); return s.verify(signatureBytes); } - byte[] createSignatureFor(String algorithm, PrivateKey privateKey, byte[] contentBytes) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + /** + * Create signature for JWT header and payload using a private key. + * + * @param algorithm algorithm name. + * @param privateKey the private key to use for signing. + * @param headerBytes JWT header. + * @param payloadBytes JWT payload. + * @return the signature bytes. + * @throws NoSuchAlgorithmException if the algorithm is not supported. + * @throws InvalidKeyException if the given key is inappropriate for initializing the specified algorithm. + * @throws SignatureException if this signature object is not initialized properly + * or if this signature algorithm is unable to process the input data provided. + */ + byte[] createSignatureFor( + String algorithm, + PrivateKey privateKey, + byte[] headerBytes, + byte[] payloadBytes + ) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + final Signature s = Signature.getInstance(algorithm); + s.initSign(privateKey); + s.update(headerBytes); + s.update(JWT_PART_SEPARATOR); + s.update(payloadBytes); + return s.sign(); + } + + /** + * 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); + } + + /** + * 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 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. + */ + 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); + } + + /** + * 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 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. + */ + + 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 08ff2885..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,15 +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.nio.charset.StandardCharsets; +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; @@ -20,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."); @@ -30,30 +36,46 @@ 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[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); - 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, contentBytes, 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); } } + @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + ECPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); + } + byte[] signature = crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes); + return DERToJOSE(signature); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + throw new SignatureGenerationException(this, e); + } + } + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { @@ -100,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; @@ -140,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) { @@ -182,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 afe1b30e..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,26 +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.CharEncoding; -import org.apache.commons.codec.binary.Base64; -import java.io.UnsupportedEncodingException; 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; } @@ -30,33 +35,41 @@ class HMACAlgorithm extends Algorithm { this(new CryptoHelper(), id, algorithm, secretBytes); } - HMACAlgorithm(String id, String algorithm, String secret) throws IllegalArgumentException, UnsupportedEncodingException { + HMACAlgorithm(String id, String algorithm, String secret) throws IllegalArgumentException { this(new CryptoHelper(), id, algorithm, getSecretBytes(secret)); } //Visible for testing - static byte[] getSecretBytes(String secret) throws IllegalArgumentException, UnsupportedEncodingException { + static byte[] getSecretBytes(String secret) throws IllegalArgumentException { if (secret == null) { throw new IllegalArgumentException("The Secret cannot be null"); } - return secret.getBytes(CharEncoding.UTF_8); + return secret.getBytes(StandardCharsets.UTF_8); } @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { - boolean valid = crypto.verifySignatureFor(getDescription(), secret, contentBytes, signatureBytes); + 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); } } + @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + return crypto.createSignatureFor(getDescription(), secret, headerBytes, payloadBytes); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new SignatureGenerationException(this, e); + } + } + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { @@ -65,5 +78,4 @@ public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { throw new SignatureGenerationException(this, e); } } - } diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java index 136cf886..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,12 +13,22 @@ 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); } } + @Override + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + return new byte[0]; + } + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { return new byte[0]; diff --git a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java index b423e159..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,23 +39,36 @@ class RSAAlgorithm extends Algorithm { @Override public void verify(DecodedJWT jwt) throws SignatureVerificationException { - byte[] contentBytes = String.format("%s.%s", jwt.getHeader(), jwt.getPayload()).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = Base64.decodeBase64(jwt.getSignature()); - try { + 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, contentBytes, 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 + public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException { + try { + RSAPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); + } + return crypto.createSignatureFor(getDescription(), privateKey, headerBytes, payloadBytes); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { + throw new SignatureGenerationException(this, e); + } + } + @Override public byte[] sign(byte[] contentBytes) throws SignatureGenerationException { try { diff --git a/lib/src/main/java/com/auth0/jwt/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 277724fd..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,10 +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 java.io.Serializable; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import static com.auth0.jwt.impl.JsonNodeClaim.extractClaim; @@ -13,19 +14,30 @@ /** * The BasicHeader class implements the Header interface. */ -class BasicHeader implements Header { +class BasicHeader implements Header, Serializable { + private static final long serialVersionUID = -4659137688548605095L; + private final String algorithm; private final String type; private final String contentType; private final String keyId; private final Map tree; + private final ObjectCodec objectCodec; - BasicHeader(String algorithm, String type, String contentType, String keyId, Map tree) { + 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.tree = tree == null ? Collections.emptyMap() : Collections.unmodifiableMap(tree); + this.objectCodec = objectCodec; } Map getTree() { @@ -54,6 +66,6 @@ public String getKeyId() { @Override public Claim getHeaderClaim(String name) { - return extractClaim(name, tree); + 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 d42db420..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,6 +1,8 @@ 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; @@ -10,29 +12,32 @@ import java.io.IOException; import java.util.Map; -class HeaderDeserializer extends StdDeserializer { +/** + * Jackson deserializer implementation for converting from JWT Header parts. + *

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

{ HeaderDeserializer() { - this(null); - } - - private HeaderDeserializer(Class vc) { - super(vc); + 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); + 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 45854785..022520f5 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JWTParser.java @@ -6,34 +6,62 @@ import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; - import java.io.IOException; +/** + * This class helps in decoding the Header and Payload of the JWT using + * {@link HeaderSerializer} and {@link PayloadSerializer}. + */ public class JWTParser implements JWTPartsParser { - private ObjectMapper mapper; + 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.mapper = mapper; + + this.payloadReader = mapper.readerFor(Payload.class); + this.headerReader = mapper.readerFor(Header.class); } @Override public Payload parsePayload(String json) throws JWTDecodeException { - return convertFromJSON(json, Payload.class); + if (json == null) { + throw decodeException(); + } + + try { + return payloadReader.readValue(json); + } catch (IOException e) { + throw decodeException(json); + } } @Override public Header parseHeader(String json) throws JWTDecodeException { - return convertFromJSON(json, Header.class); + if (json == null) { + throw decodeException(); + } + + try { + return headerReader.readValue(json); + } catch (IOException e) { + throw decodeException(json); + } } - private void addDeserializers(ObjectMapper mapper) { + static void addDeserializers(ObjectMapper mapper) { SimpleModule module = new SimpleModule(); module.addDeserializer(Payload.class, new PayloadDeserializer()); module.addDeserializer(Header.class, new HeaderDeserializer()); @@ -41,25 +69,24 @@ private void addDeserializers(ObjectMapper mapper) { } 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; } - @SuppressWarnings("WeakerAccess") - T convertFromJSON(String json, Class tClazz) throws JWTDecodeException { - if (json == null) { - throw exceptionForInvalidJson(null); - } - try { - return mapper.readValue(json, tClazz); - } catch (IOException e) { - throw exceptionForInvalidJson(json); - } + private static JWTDecodeException decodeException() { + return decodeException(null); } - private JWTDecodeException exceptionForInvalidJson(String json) { + private static JWTDecodeException decodeException(String json) { return new JWTDecodeException(String.format("The string '%s' doesn't have a valid JSON format.", json)); } } diff --git a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java index 98e76a21..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,12 +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 java.io.IOException; import java.lang.reflect.Array; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -20,76 +21,87 @@ */ class JsonNodeClaim implements Claim { + private final ObjectCodec codec; private final JsonNode data; - private JsonNodeClaim(JsonNode node) { + private JsonNodeClaim(JsonNode node, ObjectCodec codec) { this.data = node; + 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] = getObjectMapper().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(getObjectMapper().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; @@ -97,33 +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>() { - }; - ObjectMapper thisMapper = getObjectMapper(); - JsonParser thisParser = thisMapper.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 getObjectMapper().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(); } /** @@ -131,28 +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) { + static Claim extractClaim(String claimName, Map tree, ObjectCodec objectCodec) { JsonNode node = tree.get(claimName); - return claimFromNode(node); + 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) { - if (node == null || node.isNull() || node.isMissingNode()) { - return new NullClaim(); - } - return new JsonNodeClaim(node); + static Claim claimFromNode(JsonNode node, ObjectCodec objectCodec) { + return new JsonNodeClaim(node, objectCodec); } - //Visible for testing - ObjectMapper getObjectMapper() { - return new ObjectMapper(); - } -} +} \ 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 38068872..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,26 +1,32 @@ 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; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; 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 { PayloadDeserializer() { - this(null); - } - - private PayloadDeserializer(Class vc) { - super(vc); + super(Payload.class); } @Override @@ -31,31 +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); + 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()); } - ObjectMapper mapper = new ObjectMapper(); List list = new ArrayList<>(node.size()); for (int i = 0; i < node.size(); i++) { try { - list.add(mapper.treeToValue(node.get(i), String.class)); + list.add(codec.treeToValue(node.get(i), String.class)); } catch (JsonProcessingException e) { throw new JWTDecodeException("Couldn't map the Claim's array contents to String", e); } @@ -63,13 +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() || !node.canConvertToLong()) { + if (node == null || node.isNull()) { return null; } - final long ms = node.asLong() * 1000; - return new Date(ms); + if (!node.canConvertToLong()) { + throw new JWTDecodeException( + String.format("The claim '%s' contained a non-numeric date value.", claimName)); + } + 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 dced3062..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,34 +2,60 @@ 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 java.util.*; +import java.io.Serializable; +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 { +class PayloadImpl implements Payload, Serializable { + + private static final long serialVersionUID = 1659021498824562311L; + 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 ObjectCodec objectCodec; - PayloadImpl(String issuer, String subject, List audience, Date expiresAt, Date notBefore, Date issuedAt, String jwtId, Map tree) { + 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 = Collections.unmodifiableMap(tree == null ? new HashMap() : tree); + this.tree = tree != null ? Collections.unmodifiableMap(tree) : Collections.emptyMap(); + this.objectCodec = objectCodec; } Map getTree() { @@ -53,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; @@ -73,14 +115,14 @@ public String getId() { @Override public Claim getClaim(String name) { - return extractClaim(name, tree); + return extractClaim(name, tree, objectCodec); } @Override public Map getClaims() { - Map claims = new HashMap<>(); + Map claims = new HashMap<>(tree.size() * 2); for (String name : tree.keySet()) { - claims.put(name, extractClaim(name, tree)); + claims.put(name, extractClaim(name, tree, 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 29f287eb..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,60 +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.HashMap; +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 { - HashMap safePayload = new HashMap<>(); - for (Map.Entry e : holder.getClaims().entrySet()) { - switch (e.getKey()) { - case PublicClaims.AUDIENCE: - if (e.getValue() instanceof String) { - safePayload.put(e.getKey(), e.getValue()); - break; - } - String[] audArray = (String[]) e.getValue(); - if (audArray.length == 1) { - safePayload.put(e.getKey(), audArray[0]); - } else if (audArray.length > 1) { - safePayload.put(e.getKey(), audArray); + /** + * 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; - case PublicClaims.EXPIRES_AT: - case PublicClaims.ISSUED_AT: - case PublicClaims.NOT_BEFORE: - safePayload.put(e.getKey(), dateToSeconds((Date) e.getValue())); - break; - default: - if (e.getValue() instanceof Date) { - safePayload.put(e.getKey(), dateToSeconds((Date) e.getValue())); - } else { - safePayload.put(e.getKey(), 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.writeObject(safePayload); - } - - 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 893becd2..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,37 +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 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 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. @@ -94,9 +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 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 new file mode 100644 index 00000000..2756ddd8 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/JWTVerifier.java @@ -0,0 +1,40 @@ +package com.auth0.jwt.interfaces; + +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. + * + * @param token to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ + DecodedJWT verify(String token) throws JWTVerificationException; + + /** + * Performs the verification against the given {@link DecodedJWT}. + * + * @param jwt to verify. + * @return a verified and decoded JWT. + * @throws JWTVerificationException if any of the verification steps fail + */ + DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException; +} diff --git a/lib/src/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 465ebe5d..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,40 +2,266 @@ import com.auth0.jwt.JWTVerifier; +import java.time.Instant; import java.util.Date; +import java.util.function.BiPredicate; +/** + * 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 { - Verification withIssuer(String issuer); + /** + * 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}); + } + + /** + * 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. + */ + Verification withIssuer(String... issuer); + + /** + * 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. + */ Verification withSubject(String subject); + /** + * 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. + */ Verification withAudience(String... audience); + /** + * 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. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptLeeway(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid. + * Expiration Date is always verified when the value is present. + * This method overrides the value set with acceptLeeway + * + * @param leeway the window in seconds in which the Expires At Claim will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptExpiresAt(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid. + * Not Before Date is always verified when the value is present. + * This method overrides the value set with acceptLeeway + * + * @param leeway the window in seconds in which the Not Before Claim will still be valid. + * @return this same Verification instance. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptNotBefore(long leeway) throws IllegalArgumentException; + /** + * Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid. + * 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. + * @throws IllegalArgumentException if leeway is negative. + */ Verification acceptIssuedAt(long leeway) throws IllegalArgumentException; + /** + * 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 + * @return this same Verification instance. + */ Verification withJWTId(String jwtId); + /** + * 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 {@code null}. + */ Verification withClaim(String name, Boolean value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withClaim(String name, Integer value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withClaim(String name, Long value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withClaim(String name, Double value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withClaim(String name, String value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withClaim(String name, Date value) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withArrayClaim(String name, String... items) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException; + /** + * 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 {@code null}. + */ + + Verification withArrayClaim(String name, Long ... items) throws IllegalArgumentException; + + /** + * Skip the Issued At ("iat") claim verification. By default, the verification is performed. + * + * @return this same Verification instance. + */ + Verification ignoreIssuedAt(); + + /** + * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. + * + * @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 ab263bad..53cd267b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -3,7 +3,8 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.ECDSAKeyProvider; import com.auth0.jwt.interfaces.RSAKeyProvider; -import org.apache.commons.codec.binary.Base64; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -11,13 +12,12 @@ import java.nio.charset.StandardCharsets; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.RSAPrivateKey; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.time.Instant; +import java.util.*; -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; @@ -26,42 +26,165 @@ 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.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() { + Map nullMap = null; + String nullString = null; + String signed = JWTCreator.init() + .withHeader(nullMap) + .withHeader(nullString) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + } + + @Test + public void shouldSupportJsonValueHeaderWithNestedDataStructure() { + String stringClaim = "someClaim"; + Integer intClaim = 1; + List nestedListClaims = Arrays.asList("1", "2"); + String claimsJson = "{\"stringClaim\": \"someClaim\", \"intClaim\": 1, \"nestedClaim\": { \"listClaim\": [ \"1\", \"2\" ]}}"; + + String jwt = JWTCreator.init() + .withHeader(claimsJson) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + + assertThat(headerJson, JsonMatcher.hasEntry("stringClaim", stringClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("intClaim", intClaim)); + assertThat(headerJson, JsonMatcher.hasEntry("listClaim", nestedListClaims)); + } + + @Test + public void shouldFailWithIllegalArgumentExceptionForInvalidJsonForHeaderClaims() { + String invalidJson = "{ invalidJson }"; + + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid header JSON"); + + JWTCreator.init() + .withHeader(invalidJson) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() { + Map header = new HashMap<>(); + header.put(HeaderParams.KEY_ID, "xyz"); + String signed = JWTCreator.init() + .withKeyId("abc") .withHeader(header) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); String[] parts = signed.split("\\."); - String headerJson = new String(Base64.decodeBase64(parts[0]), StandardCharsets.UTF_8); - assertThat(headerJson, JsonMatcher.hasEntry("asd", 123)); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } + @Test - public void shouldAddKeyId() throws Exception { + public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() { + Map header = new HashMap<>(); + header.put(HeaderParams.KEY_ID, "xyz"); + + String signed = JWTCreator.init() + .withHeader(header) + .withKeyId("abc") + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "abc")); + } + + @Test + public void shouldRemoveHeaderIfTheValueIsNull() { + Map header = new HashMap<>(); + header.put(HeaderParams.KEY_ID, null); + header.put("test2", "isSet"); + + String signed = JWTCreator.init() + .withKeyId("test") + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + 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() { 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")); } @@ -77,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")); } @@ -94,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")); } @@ -110,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")); } @@ -127,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")); @@ -142,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")); @@ -152,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")); @@ -170,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")); @@ -180,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")); @@ -190,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")); @@ -200,50 +344,62 @@ 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 shouldSetCorrectTypeInTheHeader() 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 shouldSetEmptySignatureIfAlgorithmIsNone() throws Exception { + public void shouldSetCustomTypeInTheHeader() { + Map header = Collections.singletonMap("typ", "passport"); + String signed = JWTCreator.init() + .withHeader(header) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("typ", "passport")); + } + + @Test + public void shouldSetEmptySignatureIfAlgorithmIsNone() { String signed = JWTCreator.init() .sign(Algorithm.none()); assertThat(signed, is(notNullValue())); @@ -251,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() @@ -259,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")); @@ -270,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")); @@ -281,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")); @@ -292,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")); @@ -303,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")); @@ -314,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) @@ -326,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")); @@ -337,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")); @@ -348,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")); @@ -357,4 +525,536 @@ public void shouldAcceptCustomArrayClaimOfTypeLong() throws Exception { String[] parts = jwt.split("\\."); assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); } -} \ No newline at end of file + + @Test + public void shouldAcceptCustomClaimOfTypeMap() { + Map data = new HashMap<>(); + data.put("test1", "abc"); + data.put("test2", "def"); + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJkYXRhIjp7InRlc3QyIjoiZGVmIiwidGVzdDEiOiJhYmMifX0")); + } + + @Test + public void shouldRefuseCustomClaimOfTypeUserPojo() { + Map data = new HashMap<>(); + data.put("test1", new UserPojo("Michael", 255)); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptCustomMapClaimOfBasicObjectTypes() throws Exception { + Map data = new HashMap<>(); + + // simple types + data.put("string", "abc"); + data.put("integer", 1); + data.put("long", Long.MAX_VALUE); + data.put("double", 123.456d); + data.put("date", new Date(123000L)); + data.put("instant", Instant.ofEpochSecond(123)); + data.put("boolean", true); + + // array types + data.put("intArray", new Integer[]{3, 5}); + data.put("longArray", new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); + data.put("stringArray", new String[]{"string"}); + + data.put("list", Arrays.asList("a", "b", "c")); + + Map sub = new HashMap<>(); + sub.put("subKey", "subValue"); + + data.put("map", sub); + + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + Map map = (Map) mapper.readValue(body, Map.class).get("data"); + + assertThat(map.get("string"), is("abc")); + assertThat(map.get("integer"), is(1)); + assertThat(map.get("long"), is(Long.MAX_VALUE)); + assertThat(map.get("double"), is(123.456d)); + + assertThat(map.get("date"), is(123)); + assertThat(map.get("instant"), is(123)); + assertThat(map.get("boolean"), is(true)); + + // array types + 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"))); + assertThat(map.get("map"), is(sub)); + + } + + @SuppressWarnings("unchecked") + @Test + public void shouldAcceptCustomListClaimOfBasicObjectTypes() throws Exception { + List data = new ArrayList<>(); + + // simple types + data.add("abc"); + data.add(1); + data.add(Long.MAX_VALUE); + data.add(123.456d); + data.add(new Date(123000L)); + data.add(Instant.ofEpochSecond(123)); + data.add(true); + + // array types + data.add(new Integer[]{3, 5}); + data.add(new Long[]{Long.MAX_VALUE, Long.MIN_VALUE}); + data.add(new String[]{"string"}); + + data.add(Arrays.asList("a", "b", "c")); + + Map sub = new HashMap<>(); + sub.put("subKey", "subValue"); + + data.add(sub); + + String jwt = JWTCreator.init() + .withClaim("data", data) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + + String body = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + List list = (List) mapper.readValue(body, Map.class).get("data"); + + assertThat(list.get(0), is("abc")); + assertThat(list.get(1), is(1)); + assertThat(list.get(2), is(Long.MAX_VALUE)); + assertThat(list.get(3), is(123.456d)); + assertThat(list.get(4), is(123)); + assertThat(list.get(5), is(123)); + assertThat(list.get(6), is(true)); + + // array types + 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(10), is(Arrays.asList("a", "b", "c"))); + assertThat(list.get(11), is(sub)); + } + + @Test + public void shouldAcceptCustomClaimForNullListItem() { + Map data = new HashMap<>(); + data.put("test1", Arrays.asList("a", null, "c")); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomClaimForNullMapKey() { + Map data = new HashMap<>(); + data.put(null, "subValue"); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", data) + .sign(Algorithm.HMAC256("secret")); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void shouldRefuseCustomMapClaimForNonStringKey() { + Map data = new HashMap<>(); + data.put(new Object(), "value"); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("pojo", (Map) data) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownListElement() { + List list = Collections.singletonList(new UserPojo("Michael", 255)); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() { + List list = Collections.singletonList(new UserPojo("Michael", 255)); + + Map data = new HashMap<>(); + data.put("someList", list); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void shouldRefuseCustomListClaimForUnknownArrayType() { + List list = new ArrayList<>(); + list.add(new Object[]{"test"}); + + exception.expect(IllegalArgumentException.class); + + JWTCreator.init() + .withClaim("list", list) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldAddBasicClaim() { + Map payload = new HashMap<>(); + payload.put("asd", 123); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.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() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldAllowNullListItems() { + Map payload = new HashMap<>(); + payload.put("list", Arrays.asList("item1", null, "item2")); + String jwt = JWTCreator.init() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + String payloadJson = new String(Base64.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() + .withPayload(payload) + .sign(Algorithm.HMAC256("secret")); + } + + @Test + public void withPayloadShouldNotAllowMapWithCustomType() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Claim values must only be of types Map, List, Boolean, Integer, Long, Double, String, 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() + .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()); + } + + @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 a482685e..cc427d60 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -1,19 +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; @@ -24,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")); @@ -32,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); @@ -55,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); @@ -63,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())); @@ -74,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())); @@ -206,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())); @@ -214,68 +236,92 @@ 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 shouldGetCustomArrayClaimOfTypeString() throws Exception { + 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() { 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 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() throws Exception { - DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOiIxMjM0NTY3ODkwIiwiaWF0IjoiMTIzNDU2Nzg5MCIsIm5iZiI6IjEyMzQ1Njc4OTAiLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.TX9Ct4feGp9YyeGK9Zl91tO0YBOrguJ4As9jeqgHdZQ"); + public void shouldGetAvailableClaims() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwibmJmIjoxMjM0NTY3ODkwLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.2_0nxDPJwOk64U5V5V9pt8U92jTPJbGsHYQ35HYhbdE"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaims(), is(notNullValue())); assertThat(jwt.getClaims(), is(instanceOf(Map.class))); @@ -289,12 +335,82 @@ public void shouldGetAvailableClaims() throws Exception { assertThat(jwt.getClaims().get("extraClaim"), is(notNullValue())); } + @Test + public void shouldSerializeAndDeserialize() throws Exception { + DecodedJWT originalJwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEyMzQ1Njc4OTAsImlhdCI6MTIzNDU2Nzg5MCwibmJmIjoxMjM0NTY3ODkwLCJqdGkiOiJodHRwczovL2p3dC5pby8iLCJhdWQiOiJodHRwczovL2RvbWFpbi5hdXRoMC5jb20iLCJzdWIiOiJsb2dpbiIsImlzcyI6ImF1dGgwIiwiZXh0cmFDbGFpbSI6IkpvaG4gRG9lIn0.2_0nxDPJwOk64U5V5V9pt8U92jTPJbGsHYQ35HYhbdE"); + + assertThat(originalJwt, is(instanceOf(Serializable.class))); + + byte[] serialized = serialize(originalJwt); + DecodedJWT deserializedJwt = (DecodedJWT) deserialize(serialized); + + assertThat(originalJwt.getHeader(), is(equalTo(deserializedJwt.getHeader()))); + assertThat(originalJwt.getPayload(), is(equalTo(deserializedJwt.getPayload()))); + assertThat(originalJwt.getSignature(), is(equalTo(deserializedJwt.getSignature()))); + assertThat(originalJwt.getToken(), is(equalTo(deserializedJwt.getToken()))); + assertThat(originalJwt.getAlgorithm(), is(equalTo(deserializedJwt.getAlgorithm()))); + assertThat(originalJwt.getAudience(), is(equalTo(deserializedJwt.getAudience()))); + assertThat(originalJwt.getContentType(), is(equalTo(deserializedJwt.getContentType()))); + assertThat(originalJwt.getExpiresAt(), is(equalTo(deserializedJwt.getExpiresAt()))); + assertThat(originalJwt.getId(), is(equalTo(deserializedJwt.getId()))); + assertThat(originalJwt.getIssuedAt(), is(equalTo(deserializedJwt.getIssuedAt()))); + assertThat(originalJwt.getIssuer(), is(equalTo(deserializedJwt.getIssuer()))); + assertThat(originalJwt.getKeyId(), is(equalTo(deserializedJwt.getKeyId()))); + assertThat(originalJwt.getNotBefore(), is(equalTo(deserializedJwt.getNotBefore()))); + assertThat(originalJwt.getSubject(), is(equalTo(deserializedJwt.getSubject()))); + assertThat(originalJwt.getType(), is(equalTo(deserializedJwt.getType()))); + assertThat(originalJwt.getClaims().get("extraClaim").asString(), + is(equalTo(deserializedJwt.getClaims().get("extraClaim").asString()))); + } + + @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)); } + private static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + ObjectOutputStream o = new ObjectOutputStream(b); + o.writeObject(obj); + return b.toByteArray(); + } + + private static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException { + ByteArrayInputStream b = new ByteArrayInputStream(bytes); + ObjectInputStream o = new ObjectInputStream(b); + return o.readObject(); + } + } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/JWTTest.java b/lib/src/test/java/com/auth0/jwt/JWTTest.java index 31df4494..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,34 +33,63 @@ 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); assertThat(jwt, is(notNullValue())); } + @Test + public void shouldDecodeAStringTokenUsingInstance() { + String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + JWT jwt = new JWT(); + DecodedJWT decodedJWT = jwt.decodeJwt(token); + + assertThat(decodedJWT, is(notNullValue())); + } + // getToken @Test - public void shouldGetStringToken() throws Exception { + public void shouldGetStringToken() { DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getToken(), is(notNullValue())); assertThat(jwt.getToken(), is("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); } + // getToken + @Test + public void shouldGetStringTokenUsingInstance() { + JWT jwt = new JWT(); + DecodedJWT decodedJWT = jwt.decodeJwt("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + assertThat(decodedJWT, is(notNullValue())); + assertThat(decodedJWT.getToken(), is(notNullValue())); + assertThat(decodedJWT.getToken(), is("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ")); + } // Verify @Test - public void shouldAcceptNoneAlgorithm() throws Exception { + public void shouldVerifyDecodedToken() throws Exception { + String token = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; + DecodedJWT decodedJWT = JWT.decode(token); + RSAKey key = (RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"); + DecodedJWT jwt = JWT.require(Algorithm.RSA512(key)) + .build() + .verify(decodedJWT); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldAcceptNoneAlgorithm() { String token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9."; DecodedJWT jwt = JWT.require(Algorithm.none()) .build() @@ -69,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() @@ -79,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() @@ -89,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() @@ -168,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() @@ -182,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() @@ -193,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() @@ -204,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() @@ -215,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() @@ -223,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() @@ -235,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")); @@ -269,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")); @@ -287,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 @@ -305,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() @@ -316,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() @@ -327,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() @@ -338,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() @@ -354,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")); @@ -370,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")); @@ -386,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")); @@ -407,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")); @@ -423,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")); @@ -439,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")); @@ -455,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")); @@ -471,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")); @@ -487,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 d5826c24..732d6365 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -1,69 +1,130 @@ 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 - public void shouldThrowOnInvalidIssuer() throws Exception { - exception.expect(InvalidClaimException.class); - exception.expectMessage("The Claim 'iss' value doesn't match the required one."); - String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withIssuer("invalid") - .build() - .verify(token); + public void shouldValidateMultipleIssuers() { + String auth0Token = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; + String otherIssuertoken = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJvdGhlcklzc3VlciJ9.k4BCOJJl-c0_Y-49VD_mtt-u0QABKSV5i3W-RKc74co"; + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withIssuer("otherIssuer", "auth0") + .build(); + + assertThat(verifier.verify(auth0Token), is(notNullValue())); + assertThat(verifier.verify(otherIssuertoken), is(notNullValue())); } @Test - public void shouldValidateSubject() throws Exception { + 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() { + 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)); + } + + @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() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withSubject("1234567890") @@ -74,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") @@ -94,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") @@ -104,8 +170,52 @@ public void shouldValidateAudience() throws Exception { } @Test - public void shouldAcceptPartialAudience() throws Exception { - //Token 'aud' = ["Mark", "David", "John"] + 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 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") @@ -116,18 +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 shouldThrowOnNullCustomClaimName() throws Exception { + 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"})); + } + + @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"})); + } + + @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)); + } + + @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 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})); + } + + @Test + public void shouldThrowWhenExpectedEmptyList() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + // Token 'aud': 'wide audience' + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ3aWRlIGF1ZGllbmNlIn0.c9anq03XepcuEKWEVsPk9cck0sIIfrT6hHbBsCar49o"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAnyOfAudience(new String[0]) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'aud' value doesn't contain the required audience.")); + assertThat(e.getClaimName(), is(RegisteredClaims.AUDIENCE)); + assertThat(e.getClaimValue().asString(), is("wide audience")); + } + + @Test + public void shouldNotReplaceWhenMultipleChecksAreAdded() { + JWTVerifier verifier = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withAudience((String[]) null) + .withAudience() + .withAnyOfAudience((String[]) null) + .withAnyOfAudience() + .build(); + + assertThat(verifier.expectedChecks.size(), is(7)); //3 extra mandatory checks exp, nbf, iat + } + + @Test + public void shouldThrowOnNullCustomClaimName() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Custom Claim's name can't be null."); JWTVerifier.init(Algorithm.HMAC256("secret")) @@ -135,74 +346,118 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { } @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 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 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 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 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 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 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 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() { + 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 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 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 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 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 shouldValidateCustomClaimOfTypeString() throws Exception { + 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() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidmFsdWUifQ.Jki8pvw6KGbxpMinufrgo6RDL1cu7AtNMJYVh6t-_cE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", "value") @@ -213,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) @@ -224,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) @@ -235,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) @@ -246,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) @@ -257,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() @@ -269,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") @@ -280,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) @@ -290,81 +556,114 @@ public void shouldValidateCustomArrayClaimOfTypeInteger() throws Exception { assertThat(jwt, is(notNullValue())); } + @Test + public void shouldValidateCustomArrayClaimOfTypeLong() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbNTAwMDAwMDAwMDAxLDUwMDAwMDAwMDAwMiw1MDAwMDAwMDAwMDNdfQ.vzV7S0gbV9ZAVxChuIt4XZuSVTxMH536rFmoHzxmayM"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 500000000001L, 500000000002L, 500000000003L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 1L, 2L, 3L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSw1MDAwMDAwMDAwMDIsNTAwMDAwMDAwMDAzXX0.PQjb2rPPpYjM2sItZEzZcjS2YbfPCp6xksTSPjpjTQA"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withArrayClaim("name", 1L, 500000000002L, 500000000003L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + // Generic Delta - @SuppressWarnings("RedundantCast") @Test - 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); @@ -373,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); @@ -427,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); @@ -478,52 +790,72 @@ public void shouldThrowOnNegativeNotBeforeLeeway() throws Exception { .acceptNotBefore(-1); } - // Issued At + // Issued At with future date @Test - public void shouldValidateIssuedAtWithLeeway() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE - 1000)); + 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)); + } - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) - .acceptIssuedAt(2); - DecodedJWT jwt = verification - .build(clock) - .verify(token); + // Issued At with future date and ignore flag + @Test + 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(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)); + 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() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0Nzc1OTJ9.0WJky9eLN7kuxLyZlmbcXRL3Wy8hLoNCEk5CCl2M4lo"; - JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); - verification - .build(clock) + 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); @@ -532,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") @@ -543,30 +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(null) + .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.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks.size(), is(5)); } @Test - public void shouldSkipClaimValidationsIfNoClaimsRequired() throws Exception { + public void shouldNotRemoveIssuerWhenPassingNullReference() { + Algorithm algorithm = mock(Algorithm.class); + JWTVerifier verifier = JWTVerifier.init(algorithm) + .withIssuer((String) null) + .build(); + + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks.size(), is(4)); + + verifier = JWTVerifier.init(algorithm) + .withIssuer((String[]) null) + .build(); + + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks.size(), is(4)); + + verifier = JWTVerifier.init(algorithm) + .withIssuer() + .build(); + + assertThat(verifier.expectedChecks, is(notNullValue())); + assertThat(verifier.expectedChecks.size(), is(4)); + + String emptyIss = " "; + verifier = JWTVerifier.init(algorithm) + .withIssuer(emptyIss) + .build(); + + assertThat(verifier.expectedChecks, is(notNullValue())); + } + + @Test + public void shouldSkipClaimValidationsIfNoClaimsRequired() { String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .build() @@ -574,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/JsonMatcher.java b/lib/src/test/java/com/auth0/jwt/JsonMatcher.java index f03547d4..b09ab187 100644 --- a/lib/src/test/java/com/auth0/jwt/JsonMatcher.java +++ b/lib/src/test/java/com/auth0/jwt/JsonMatcher.java @@ -68,6 +68,10 @@ public static JsonMatcher hasEntry(String key, Matcher valueMatcher) { return new JsonMatcher(key, null, valueMatcher); } + public static JsonMatcher isNotPresent(String key) { + return new JsonMatcher(key, null, null); + } + private String getStringKey(String key) { return "\"" + key + "\":"; } 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 6dcbbae9..e09661d3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -5,14 +5,18 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.security.interfaces.*; 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; +import static org.mockito.Mockito.when; public class AlgorithmTest { @@ -21,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; @@ -29,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; @@ -37,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; @@ -45,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; @@ -53,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; @@ -61,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; @@ -69,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; @@ -77,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; @@ -92,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; @@ -100,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; @@ -115,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; @@ -123,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; @@ -138,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; @@ -146,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; @@ -161,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; @@ -169,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; @@ -184,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; @@ -192,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; @@ -207,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())); @@ -217,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())); @@ -227,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())); @@ -237,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())); @@ -247,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())); @@ -257,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())); @@ -267,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); @@ -278,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); @@ -289,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); @@ -301,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); @@ -312,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); @@ -323,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); @@ -334,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); @@ -346,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); @@ -357,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); @@ -368,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); @@ -379,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); @@ -391,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); @@ -402,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); @@ -413,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); @@ -424,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); @@ -436,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); @@ -447,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); @@ -458,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); @@ -469,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); @@ -481,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); @@ -492,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); @@ -503,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); @@ -514,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); @@ -526,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); @@ -537,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())); @@ -546,4 +550,27 @@ public void shouldCreateNoneAlgorithm() throws Exception { assertThat(algorithm.getName(), is("none")); } + @Test + public void shouldForwardHeaderPayloadSignatureToSiblingSignMethodForBackwardsCompatibility() throws Exception { + Algorithm algorithm = mock(Algorithm.class); + + ArgumentCaptor contentCaptor = ArgumentCaptor.forClass(byte[].class); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + byte[] signature = new byte[]{0x10, 0x11, 0x12}; + when(algorithm.sign(any(byte[].class), any(byte[].class))).thenCallRealMethod(); + when(algorithm.sign(contentCaptor.capture())).thenReturn(signature); + + byte[] sign = algorithm.sign(header, payload); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(sign, is(signature)); + assertThat(contentCaptor.getValue(), is(bout.toByteArray())); + } } \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java new file mode 100644 index 00000000..ef8e65e8 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/algorithms/CryptoTestHelper.java @@ -0,0 +1,35 @@ +package com.auth0.jwt.algorithms; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; + +public abstract class CryptoTestHelper { + + private static final Pattern authHeaderPattern = Pattern.compile("^([\\w-]+)\\.([\\w-]+)\\.([\\w-]+)"); + + public static String asJWT(Algorithm algorithm, String header, String payload) { + byte[] signatureBytes = algorithm.sign(header.getBytes(StandardCharsets.UTF_8), payload.getBytes(StandardCharsets.UTF_8)); + String jwtSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes); + return String.format("%s.%s.%s", header, payload, jwtSignature); + } + + public static void assertSignatureValue(String jwt, String expectedSignature) { + String jwtSignature = jwt.substring(jwt.lastIndexOf('.') + 1); + assertThat(jwtSignature, is(expectedSignature)); + } + + public static void assertSignaturePresent(String jwt) { + Matcher matcher = authHeaderPattern.matcher(jwt); + if (!matcher.find() || matcher.groupCount() < 3) { + fail("No signature present in " + jwt); + } + + assertThat(matcher.group(3), not(is(emptyString()))); + } +} diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java index 86bf3c0b..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,39 +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"; @@ -70,7 +75,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)); @@ -90,7 +95,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)); } @@ -106,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)); @@ -147,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)); @@ -160,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)); @@ -174,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)); @@ -195,7 +200,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)); @@ -215,7 +220,7 @@ public void shouldThrowOnECDSA384VerificationWithDERSignatureWithBothKeys() thro exception.expectCause(isA(SignatureException.class)); exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); algorithm.verify(JWT.decode(jwt)); } @@ -231,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)); @@ -272,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)); @@ -285,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)); @@ -299,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)); @@ -320,7 +325,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)); @@ -340,7 +345,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)); } @@ -356,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)); @@ -397,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)); @@ -410,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)); @@ -424,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)); @@ -439,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"); @@ -456,10 +461,14 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -474,10 +483,14 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -492,10 +505,14 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -503,36 +520,47 @@ 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"; private static final String auth0IssPayload = "eyJpc3MiOiJhdXRoMCJ9"; + private static final byte[] ES256HeaderBytes = ES256Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] ES384HeaderBytes = ES384Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] ES512HeaderBytes = ES512Header.getBytes(StandardCharsets.UTF_8); + private static final byte[] auth0IssPayloadBytes = auth0IssPayload.getBytes(StandardCharsets.UTF_8); + + @Test public void shouldDoECDSA256Signing() throws Exception { - Algorithm algorithmSign = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + byte[] signatureBytes = algorithm.sign(ES256HeaderBytes, auth0IssPayloadBytes); + String jwtSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes); + String jwt = String.format("%s.%s.%s", ES256Header, auth0IssPayload, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -544,18 +572,15 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA256(provider); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); + + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -564,7 +589,7 @@ public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -575,33 +600,25 @@ public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -613,18 +630,15 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA384(provider); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); + + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -633,7 +647,7 @@ public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -644,33 +658,27 @@ public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + String jwt = asJWT(algorithmSign, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -683,18 +691,15 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA512(provider); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); - assertThat(signatureBytes, is(notNullValue())); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -703,7 +708,7 @@ public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -714,7 +719,7 @@ public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -724,14 +729,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test @@ -741,14 +746,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test @@ -758,18 +763,18 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256HeaderBytes, new byte[0]); } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -779,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); @@ -795,7 +800,7 @@ public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequence ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; - byte[] signature = algorithm256.sign(content256.getBytes()); + byte[] signature = algorithm256.sign(content256.getBytes(), new byte[0]); signature[0] = (byte) 0x02; algorithm256.DERToJOSE(signature); } @@ -837,24 +842,23 @@ 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 public void shouldSignAndVerifyWithECDSA256() throws Exception { ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm256.sign(content256.getBytes()); - String signature256 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content256 + "." + signature256; + String jwt = asJWT(algorithm256, header256, body); algorithm256.verify(JWT.decode(jwt)); } } @@ -862,13 +866,11 @@ public void shouldSignAndVerifyWithECDSA256() throws Exception { @Test public void shouldSignAndVerifyWithECDSA384() throws Exception { ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String content384 = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm384.sign(content384.getBytes()); - String signature384 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content384 + "." + signature384; + String jwt = asJWT(algorithm384, header384, body); algorithm384.verify(JWT.decode(jwt)); } } @@ -876,13 +878,11 @@ public void shouldSignAndVerifyWithECDSA384() throws Exception { @Test public void shouldSignAndVerifyWithECDSA512() throws Exception { ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String content512 = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm512.sign(content512.getBytes()); - String signature512 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content512 + "." + signature512; + String jwt = asJWT(algorithm512, header512, body); algorithm512.verify(JWT.decode(jwt)); } } @@ -1040,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); @@ -1058,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; @@ -1106,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]; @@ -1125,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; @@ -1134,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); @@ -1159,9 +1159,194 @@ 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 + public void shouldBeEqualSignatureMethodDecodeResults() throws Exception { + // signatures are not deterministic in value, so instead of directly comparing the signatures, + // check that both sign(..) methods can be used to create a jwt which can be + // verified + Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + String header = "eyJhbGciOiJFUzI1NiJ9"; + String payload = "eyJpc3MiOiJhdXRoMCJ9"; + + byte[] headerBytes = header.getBytes(StandardCharsets.UTF_8); + byte[] payloadBytes = payload.getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(headerBytes); + bout.write('.'); + bout.write(payloadBytes); + + String jwtSignature1 = Base64.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.getUrlEncoder().withoutPadding().encodeToString(algorithm.sign(headerBytes, payloadBytes)); + String jwt2 = String.format("%s.%s.%s", header, payload, jwtSignature2); + + algorithm.verify(JWT.decode(jwt2)); + } + + /** + * Test deprecated signing method error handling. + * + * @see {@linkplain #shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull} + * @throws Exception expected exception + */ + + @Test + public void shouldFailOnECDSA256SigningWithDeprecatedMethodWhenProvidedPrivateKeyIsNull() { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.sign(new byte[0]); + } + + @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 4af1c4ec..3925eef3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -4,7 +4,6 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.ECDSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -12,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; @@ -43,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(); @@ -55,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"; @@ -87,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)); @@ -107,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)); } @@ -123,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)); @@ -164,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)); @@ -177,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)); @@ -191,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)); @@ -212,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)); @@ -232,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)); } @@ -248,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)); @@ -289,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)); @@ -302,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)); @@ -316,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)); @@ -337,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)); @@ -357,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)); } @@ -373,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)); @@ -414,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)); @@ -427,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)); @@ -441,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)); @@ -456,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"); @@ -473,10 +478,14 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -491,10 +500,14 @@ public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -509,10 +522,14 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); + 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); @@ -530,26 +547,18 @@ public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception public void shouldDoECDSA256Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -561,18 +570,15 @@ public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA256(provider); - String jwtContent = String.format("%s.%s", ES256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -581,7 +587,7 @@ public void shouldFailOnECDSA256SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -592,33 +598,25 @@ public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -630,18 +628,15 @@ public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA384(provider); - String jwtContent = String.format("%s.%s", ES384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -650,7 +645,7 @@ public void shouldFailOnECDSA384SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -661,33 +656,26 @@ public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldDoECDSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); Algorithm algorithmVerify = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithmSign, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithmVerify.verify(JWT.decode(jwt)); } @Test public void shouldDoECDSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @@ -700,18 +688,14 @@ public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); Algorithm algorithm = Algorithm.ECDSA512(provider); - String jwtContent = String.format("%s.%s", ES512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -720,7 +704,7 @@ public void shouldFailOnECDSA512SigningWhenProvidedPrivateKeyIsNull() throws Exc ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.ECDSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -731,7 +715,7 @@ public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -741,14 +725,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test @@ -758,14 +742,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test @@ -775,18 +759,18 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, provider); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8), new byte[0]); } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { ECPublicKey publicKey = mock(ECPublicKey.class); ECPrivateKey privateKey = mock(ECPrivateKey.class); ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -796,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); @@ -812,7 +796,7 @@ public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequence ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; - byte[] signature = algorithm256.sign(content256.getBytes()); + byte[] signature = algorithm256.sign(content256.getBytes(), new byte[0]); signature[0] = (byte) 0x02; algorithm256.DERToJOSE(signature); } @@ -854,24 +838,23 @@ 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 public void shouldSignAndVerifyWithECDSA256() throws Exception { ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - String content256 = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm256.sign(content256.getBytes()); - String signature256 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content256 + "." + signature256; + String jwt = asJWT(algorithm256, header256, body); algorithm256.verify(JWT.decode(jwt)); } } @@ -879,13 +862,11 @@ public void shouldSignAndVerifyWithECDSA256() throws Exception { @Test public void shouldSignAndVerifyWithECDSA384() throws Exception { ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - String content384 = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm384.sign(content384.getBytes()); - String signature384 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content384 + "." + signature384; + String jwt = asJWT(algorithm384, header384, body); algorithm384.verify(JWT.decode(jwt)); } } @@ -893,13 +874,11 @@ public void shouldSignAndVerifyWithECDSA384() throws Exception { @Test public void shouldSignAndVerifyWithECDSA512() throws Exception { ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - String content512 = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9"; + String header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; for (int i = 0; i < 10; i++) { - byte[] signature = algorithm512.sign(content512.getBytes()); - String signature512 = Base64.encodeBase64URLSafeString((signature)); - - String jwt = content512 + "." + signature512; + String jwt = asJWT(algorithm512, header512, body); algorithm512.verify(JWT.decode(jwt)); } } @@ -1054,4 +1033,5 @@ public void shouldDecodeECDSA512DER() throws Exception { assertValidJOSESignature(joseSignature, 66, true, true); } + } diff --git a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java index 2d5fd301..9b6ac0c0 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -4,19 +4,22 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import java.io.ByteArrayOutputStream; +import java.nio.charset.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; @@ -30,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)); @@ -47,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"; @@ -56,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"; @@ -65,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)); @@ -75,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"; @@ -84,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"; @@ -93,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)); @@ -103,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"; @@ -112,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"; @@ -127,7 +142,7 @@ public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() throws Exce exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); @@ -142,7 +157,7 @@ public void shouldThrowOnVerifyWhenTheSecretIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(byte[].class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); @@ -158,98 +173,74 @@ 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 jwtContent = String.format("%s.%s", HS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC384SigningWithBytes() throws Exception { + public void shouldDoHMAC384SigningWithBytes() { Algorithm algorithm = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); - String jwtContent = String.format("%s.%s", HS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC512SigningWithBytes() throws Exception { + public void shouldDoHMAC512SigningWithBytes() { Algorithm algorithm = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); - String jwtContent = String.format("%s.%s", HS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC256SigningWithString() throws Exception { + public void shouldDoHMAC256SigningWithString() { Algorithm algorithm = Algorithm.HMAC256("secret"); - String jwtContent = String.format("%s.%s", HS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC384SigningWithString() throws Exception { + public void shouldDoHMAC384SigningWithString() { Algorithm algorithm = Algorithm.HMAC384("secret"); - String jwtContent = String.format("%s.%s", HS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC512SigningWithString() throws Exception { + public void shouldDoHMAC512SigningWithString() { Algorithm algorithm = Algorithm.HMAC512("secret"); - String jwtContent = String.format("%s.%s", HS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm ,HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -260,11 +251,11 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -274,16 +265,45 @@ public void shouldThrowOnSignWhenTheSecretIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(byte[].class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test - public void shouldReturnNullSigningKeyId() throws Exception { + public void shouldReturnNullSigningKeyId() { assertThat(Algorithm.HMAC256("secret").getSigningKeyId(), is(nullValue())); } + @Test + public void shouldBeEqualSignatureMethodResults() throws Exception { + Algorithm algorithm = Algorithm.HMAC256("secret"); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); + } + + + @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 9025f9e4..115de64c 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -4,12 +4,11 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; import com.auth0.jwt.interfaces.RSAKeyProvider; -import org.apache.commons.codec.binary.Base64; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.nio.charset.StandardCharsets; +import java.io.ByteArrayOutputStream; import java.security.*; import java.security.interfaces.RSAKey; import java.security.interfaces.RSAPrivateKey; @@ -18,14 +17,14 @@ 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; import static org.mockito.Mockito.mock; 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)); @@ -213,9 +212,9 @@ public void shouldThrowWhenMacAlgorithmDoesNotExists() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: some-alg"); exception.expectCause(isA(NoSuchAlgorithmException.class)); - + CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -233,7 +232,7 @@ public void shouldThrowWhenThePublicKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -251,7 +250,7 @@ public void shouldThrowWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(byte[].class), any(byte[].class))) + when(crypto.verifySignatureFor(anyString(), any(PublicKey.class), any(String.class), any(String.class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); @@ -274,15 +273,11 @@ public void shouldDoRSA256Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA256((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS256Header, auth0IssPayload); String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -290,15 +285,11 @@ public void shouldDoRSA256Signing() throws Exception { public void shouldDoRSA256SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -310,18 +301,15 @@ public void shouldDoRSA256SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA256(provider); - String jwtContent = String.format("%s.%s", RS256Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -330,7 +318,7 @@ public void shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA256(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -341,7 +329,7 @@ public void shouldFailOnRSA256SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -349,15 +337,11 @@ public void shouldDoRSA384Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA384((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS384Header, auth0IssPayload); String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -365,15 +349,11 @@ public void shouldDoRSA384Signing() throws Exception { public void shouldDoRSA384SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA384((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -385,18 +365,15 @@ public void shouldDoRSA384SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA384(provider); - String jwtContent = String.format("%s.%s", RS384Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -405,7 +382,7 @@ public void shouldFailOnRSA384SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA384(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -416,7 +393,7 @@ public void shouldFailOnRSA384SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -424,15 +401,11 @@ public void shouldDoRSA512Signing() throws Exception { Algorithm algorithmSign = Algorithm.RSA512((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); Algorithm algorithmVerify = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithmSign, RS512Header, auth0IssPayload); String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithmVerify.verify(JWT.decode(jwt)); } @@ -440,15 +413,11 @@ public void shouldDoRSA512Signing() throws Exception { public void shouldDoRSA512SigningWithBothKeys() throws Exception { Algorithm algorithm = Algorithm.RSA512((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(jwtSignature, is(expectedSignature)); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); algorithm.verify(JWT.decode(jwt)); } @@ -460,18 +429,15 @@ public void shouldDoRSA512SigningWithProvidedPrivateKey() throws Exception { when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); Algorithm algorithm = Algorithm.RSA512(provider); - String jwtContent = String.format("%s.%s", RS512Header, auth0IssPayload); - byte[] contentBytes = jwtContent.getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String jwtSignature = Base64.encodeBase64URLSafeString(signatureBytes); - String jwt = String.format("%s.%s", jwtContent, jwtSignature); + + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); - assertThat(signatureBytes, is(notNullValue())); + assertSignaturePresent(jwt); algorithm.verify(JWT.decode(jwt)); } @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)); @@ -480,7 +446,7 @@ public void shouldFailOnRSA512SigningWhenProvidedPrivateKeyIsNull() throws Excep RSAKeyProvider provider = mock(RSAKeyProvider.class); when(provider.getPrivateKey()).thenReturn(null); Algorithm algorithm = Algorithm.RSA512(provider); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -491,7 +457,7 @@ public void shouldFailOnRSA512SigningWhenUsingPublicKey() throws Exception { exception.expectCause(hasMessage(is("The given Private Key is null."))); Algorithm algorithm = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - algorithm.sign(new byte[0]); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -501,14 +467,14 @@ public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Except exception.expectCause(isA(NoSuchAlgorithmException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(NoSuchAlgorithmException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -518,14 +484,14 @@ public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { exception.expectCause(isA(InvalidKeyException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(InvalidKeyException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test @@ -535,18 +501,18 @@ public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) .thenThrow(SignatureException.class); RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", provider); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + algorithm.sign(new byte[0], new byte[0]); } @Test - public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { RSAPublicKey publicKey = mock(RSAPublicKey.class); RSAPrivateKey privateKey = mock(RSAPrivateKey.class); RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); @@ -556,11 +522,58 @@ 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); assertThat(algorithm.getSigningKeyId(), is("keyId")); } + + @Test + public void shouldBeEqualSignatureMethodResults() throws Exception { + RSAPrivateKey privateKey = (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA"); + RSAPublicKey publicKey = (RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + + Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + + byte[] header = new byte[]{0x00, 0x01, 0x02}; + byte[] payload = new byte[]{0x04, 0x05, 0x06}; + + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bout.write(header); + bout.write('.'); + bout.write(payload); + + assertThat(algorithm.sign(bout.toByteArray()), is(algorithm.sign(header, payload))); + } + + /** + * Test deprecated signing method error handling. + * + * @see {@linkplain #shouldFailOnRSA256SigningWhenProvidedPrivateKeyIsNull} + */ + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.RSA256(provider); + algorithm.sign(new byte[0]); + } + + @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 d96baacf..c4a04d81 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java @@ -1,6 +1,8 @@ package com.auth0.jwt.impl; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.TextNode; import org.hamcrest.collection.IsMapContaining; @@ -18,36 +20,38 @@ public class BasicHeaderTest { @Rule public ExpectedException exception = ExpectedException.none(); + + private ObjectReader objectReader = new ObjectMapper().reader(); @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()); + 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); + 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); - BasicHeader header = new BasicHeader(null, null, null, null, map); + BasicHeader header = new BasicHeader(null, null, null, null, map, objectReader); assertThat(header.getTree(), is(notNullValue())); assertThat(header.getTree(), is(IsMapContaining.hasEntry("key", node))); } @Test - public void shouldGetAlgorithm() throws Exception { - BasicHeader header = new BasicHeader("HS256", null, null, null, null); + public void shouldGetAlgorithm() { + BasicHeader header = new BasicHeader("HS256", null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getAlgorithm(), is(notNullValue())); @@ -55,16 +59,16 @@ public void shouldGetAlgorithm() throws Exception { } @Test - public void shouldGetNullAlgorithmIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + public void shouldGetNullAlgorithmIfMissing() { + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getAlgorithm(), is(nullValue())); } @Test - public void shouldGetType() throws Exception { - BasicHeader header = new BasicHeader(null, "jwt", null, null, null); + public void shouldGetType() { + BasicHeader header = new BasicHeader(null, "jwt", null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getType(), is(notNullValue())); @@ -72,16 +76,16 @@ public void shouldGetType() throws Exception { } @Test - public void shouldGetNullTypeIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + public void shouldGetNullTypeIfMissing() { + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getType(), is(nullValue())); } @Test - public void shouldGetContentType() throws Exception { - BasicHeader header = new BasicHeader(null, null, "content", null, null); + public void shouldGetContentType() { + BasicHeader header = new BasicHeader(null, null, "content", null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getContentType(), is(notNullValue())); @@ -89,16 +93,16 @@ public void shouldGetContentType() throws Exception { } @Test - public void shouldGetNullContentTypeIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + public void shouldGetNullContentTypeIfMissing() { + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getContentType(), is(nullValue())); } @Test - public void shouldGetKeyId() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, "key", null); + public void shouldGetKeyId() { + BasicHeader header = new BasicHeader(null, null, null, "key", null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getKeyId(), is(notNullValue())); @@ -106,18 +110,18 @@ public void shouldGetKeyId() throws Exception { } @Test - public void shouldGetNullKeyIdIfMissing() throws Exception { - BasicHeader header = new BasicHeader(null, null, null, null, null); + public void shouldGetNullKeyIdIfMissing() { + BasicHeader header = new BasicHeader(null, null, null, null, null, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getKeyId(), is(nullValue())); } @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); + BasicHeader header = new BasicHeader(null, null, null, null, tree, objectReader); assertThat(header, is(notNullValue())); assertThat(header.getHeaderClaim("extraClaim"), is(instanceOf(JsonNodeClaim.class))); @@ -125,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); + 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 3b6af385..02d782a7 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/HeaderDeserializerTest.java @@ -21,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,9 +36,8 @@ public class HeaderDeserializerTest { public ExpectedException exception = ExpectedException.none(); private HeaderDeserializer deserializer; - @Before - public void setUp() throws Exception { + public void setUp() { deserializer = new HeaderDeserializer(); } @@ -87,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); @@ -97,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); @@ -106,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 1c990fcb..da62131a 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JWTParserTest.java @@ -5,6 +5,7 @@ import com.auth0.jwt.interfaces.Payload; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializationFeature; import org.junit.Before; import org.junit.Rule; @@ -17,6 +18,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class JWTParserTest { @@ -25,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))); @@ -38,23 +40,25 @@ 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)); } @Test public void shouldParsePayload() throws Exception { ObjectMapper mapper = mock(ObjectMapper.class); + ObjectReader reader = mock(ObjectReader.class); + when(mapper.readerFor(Payload.class)).thenReturn(reader); JWTParser parser = new JWTParser(mapper); parser.parsePayload("{}"); - verify(mapper).readValue("{}", Payload.class); + verify(reader).readValue("{}"); } @Test - 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)); @@ -65,14 +69,16 @@ public void shouldThrowOnInvalidPayload() throws Exception { @Test public void shouldParseHeader() throws Exception { ObjectMapper mapper = mock(ObjectMapper.class); + ObjectReader reader = mock(ObjectReader.class); + when(mapper.readerFor(Header.class)).thenReturn(reader); JWTParser parser = new JWTParser(mapper); parser.parseHeader("{}"); - verify(mapper).readValue("{}", Header.class); + verify(reader).readValue("{}"); } @Test - 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)); @@ -81,27 +87,30 @@ public void shouldThrowOnInvalidHeader() throws Exception { } @Test - public void shouldConvertFromValidJSON() throws Exception { - String json = "\r\n { \r\n } \r\n"; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(notNullValue())); + public void shouldThrowWhenConvertingHeaderIfNullJson() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The string 'null' doesn't have a valid JSON format."); + parser.parseHeader(null); + } + + @Test + public void shouldThrowWhenConvertingHeaderFromInvalidJson() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The string '}{' doesn't have a valid JSON format."); + parser.parseHeader("}{"); } @Test - public void shouldThrowWhenConvertingIfNullJson() throws Exception { + public void shouldThrowWhenConvertingPayloadIfNullJson() { exception.expect(JWTDecodeException.class); exception.expectMessage("The string 'null' doesn't have a valid JSON format."); - String json = null; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(nullValue())); + parser.parsePayload(null); } @Test - public void shouldThrowWhenConvertingFromInvalidJson() throws Exception { + public void shouldThrowWhenConvertingPayloadFromInvalidJson() { exception.expect(JWTDecodeException.class); exception.expectMessage("The string '}{' doesn't have a valid JSON format."); - String json = "}{"; - Object object = parser.convertFromJSON(json, Object.class); - assertThat(object, is(nullValue())); + parser.parsePayload("}{"); } -} \ No newline at end of file +} diff --git a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java index 944860f9..a0364953 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -19,29 +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; + @Rule public ExpectedException exception = ExpectedException.none(); @Before - public void setUp() throws Exception { + public void setUp() { mapper = getDefaultObjectMapper(); } @Test - public void shouldGetBooleanValue() throws Exception { + public void shouldGetBooleanValue() { JsonNode value = mapper.valueToTree(true); Claim claim = claimFromNode(value); @@ -49,8 +63,12 @@ public void shouldGetBooleanValue() throws Exception { assertThat(claim.asBoolean(), is(true)); } + private Claim claimFromNode(JsonNode value) { + 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"); @@ -58,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); @@ -67,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"); @@ -75,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); @@ -84,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); @@ -92,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); @@ -101,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"); @@ -109,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); @@ -135,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); @@ -143,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); @@ -152,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); @@ -161,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); @@ -169,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); @@ -177,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); @@ -186,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); @@ -195,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); @@ -204,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); @@ -212,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); @@ -220,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); @@ -229,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); @@ -237,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); @@ -245,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); @@ -270,10 +291,11 @@ public void shouldThrowIfAnExtraordinaryExceptionHappensWhenParsingAsGenericMap( JsonNode value = mock(ObjectNode.class); when(value.getNodeType()).thenReturn(JsonNodeType.OBJECT); - JsonNodeClaim claim = (JsonNodeClaim) claimFromNode(value); - JsonNodeClaim spiedClaim = spy(claim); ObjectMapper mockedMapper = mock(ObjectMapper.class); - when(spiedClaim.getObjectMapper()).thenReturn(mockedMapper); + + JsonNodeClaim claim = (JsonNodeClaim) JsonNodeClaim.claimFromNode(value, mockedMapper); + JsonNodeClaim spiedClaim = spy(claim); + JsonParser mockedParser = mock(JsonParser.class); when(mockedMapper.treeAsTokens(value)).thenReturn(mockedParser); when(mockedParser.readValueAs(ArgumentMatchers.any(TypeReference.class))).thenThrow(IOException.class); @@ -283,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); @@ -293,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); @@ -301,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); @@ -314,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 014ae838..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,8 +35,11 @@ public class PayloadDeserializerTest { public ExpectedException exception = ExpectedException.none(); private PayloadDeserializer deserializer; + private ObjectMapper objectMapper; + @Before - public void setUp() throws Exception { + public void setUp() { + objectMapper = new ObjectMapper(); deserializer = new PayloadDeserializer(); } @@ -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,121 +126,123 @@ 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 shouldGetNullDateWhenParsingNonNumericNode() throws Exception { + public void shouldThrowWhenParsingNonNumericNode() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The claim 'key' contained a non-numeric date value."); + Map tree = new HashMap<>(); TextNode node = new TextNode("123456789"); tree.put("key", node); - Date date = deserializer.getDateFromSeconds(tree, "key"); - assertThat(date, is(nullValue())); + deserializer.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); @@ -243,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); @@ -252,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 ca7562dc..5ad7ac68 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -2,20 +2,20 @@ import com.auth0.jwt.interfaces.Claim; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.TextNode; import org.hamcrest.collection.IsCollectionWithSize; -import org.hamcrest.core.IsCollectionContaining; +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; import static org.hamcrest.Matchers.*; @@ -25,142 +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 objectMapper; @Before - public void setUp() throws Exception { - 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); + 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()); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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())); @@ -170,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/scripts/bintray.gradle b/scripts/bintray.gradle deleted file mode 100644 index ba868123..00000000 --- a/scripts/bintray.gradle +++ /dev/null @@ -1,55 +0,0 @@ -def credentials = new Bintray(project); - -if (credentials.valid()) { - apply plugin: 'com.jfrog.bintray' - bintray { - user = credentials.user - key = credentials.key - publications = ['mavenJava'] - dryRun = project.version.endsWith("-SNAPSHOT") - publish = false - pkg { - repo = 'java' - name = 'java-jwt' - desc = 'Java implementation of JSON Web Token (JWT) ' - websiteUrl = 'https://github.com/auth0/java-jwt' - vcsUrl = 'scm:git@github.com:auth0/java-jwt.git' - licenses = ["MIT"] - userOrg = 'auth0' - publish = false - version { - gpg { - sign = true - passphrase = credentials.passphrasse - } - vcsTag = project.version - name = project.version - released = new Date() - } - } - } -} - -class Bintray { - String user - String key - String passphrasse - - Bintray(project) { - this.user = Bintray.value(project, 'BINTRAY_USER', 'bintray.user') - this.key = Bintray.value(project, 'BINTRAY_KEY', 'bintray.key') - this.passphrasse = Bintray.value(project, 'BINTRAY_PASSPHRASE', 'bintray.gpg.password') - } - - def valid() { - return this.user != null && this.key != null && this.passphrasse != null; - } - - private static def value(Project project, String env, String property) { - def value = System.getenv(env) - if (project.hasProperty(property)) { - value = project.getProperty(property); - } - return value - } -} \ No newline at end of file diff --git a/scripts/maven.gradle b/scripts/maven.gradle deleted file mode 100644 index a541b008..00000000 --- a/scripts/maven.gradle +++ /dev/null @@ -1,105 +0,0 @@ -apply plugin: Auth0OSS - -class Auth0OSS implements Plugin { - - void apply(Project target) { - target.extensions.create("auth0", Auth0Extension, target) - target.configure(target) { - apply plugin: 'maven-publish' - - target.task("sourcesJar", type: Jar, dependsOn: classes) { - classifier = 'sources' - from sourceSets.main.allSource - } - target.task("javadocJar", type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.getDestinationDir() - } - - artifacts { - archives sourcesJar, javadocJar - } - - publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar - groupId project.group - artifactId project.name - version project.version - } - } - } - - publishing.publications.all { - pom.withXml { - - def lib = project.extensions.auth0 - def root = asNode() - - root.appendNode('packaging', 'jar') - root.appendNode('name', lib.name) - root.appendNode('description', lib.description) - root.appendNode('url', lib.url) - - def developersNode = root.appendNode('developers') - project.extensions.auth0.developers.each { - def node = developersNode.appendNode('developer') - node.appendNode('id', it.id) - node.appendNode('name', it.name) - node.appendNode('email', it.email) - } - - def dependenciesNode = root.appendNode('dependencies') - - configurations.compile.allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - } - - def licenceNode = root.appendNode('licenses').appendNode('license') - licenceNode.appendNode('name', 'The MIT License (MIT)') - licenceNode.appendNode('url', "https://raw.githubusercontent.com/auth0/${lib.repo}/master/LICENSE") - licenceNode.appendNode('distribution', 'repo') - - def scmNode = root.appendNode('scm') - scmNode.appendNode('connection', "scm:git@github.com:auth0/${lib.repo}.git") - scmNode.appendNode('developerConnection', "scm:git@github.com:auth0/${lib.repo}.git") - scmNode.appendNode('url', "https://github.com/auth0/${lib.repo}") - } - } - } - } -} - -class Auth0Extension { - String name - String repo - String description - String url - List developers = [] - - private Project project - - Auth0Extension(project) { - this.project = project - } - - void developer(Closure developerClosure) { - def developer = project.configure(new Developer(), developerClosure) - developers.add(developer) - } -} - -class Developer { - String id - String name - String email -} - - - diff --git a/scripts/release.gradle b/scripts/release.gradle deleted file mode 100644 index da74e9b5..00000000 --- a/scripts/release.gradle +++ /dev/null @@ -1,170 +0,0 @@ -import java.text.SimpleDateFormat - -apply plugin: ReleasePlugin - -class Semver { - String version - def snapshot - - def getStringVersion() { - return snapshot ? "$version-SNAPSHOT" : version - } - - def nextPatch() { - def parts = version.split("\\.") - def patch = Integer.parseInt(parts[2]) + 1 - return "${parts[0]}.${parts[1]}.${patch}" - } - - def nextMinor() { - def parts = version.split("\\.") - def minor = Integer.parseInt(parts[1]) + 1 - return "${parts[0]}.${minor}.0" - } - -} - -class ChangeLogTask extends DefaultTask { - - def current - def next - - @TaskAction - def update() { - def repository = project.auth0.repo - def file = new File('CHANGELOG.md') - def output = new File('CHANGELOG.md.release') - output.newWriter().withWriter { writer -> - - file.eachLine { line, number -> - if (number == 0 && !line.startsWith('# Change Log')) { - throw new GradleException('Change Log file is not properly formatted') - } - - writer.println(line) - - if (number == 0 || number > 1) { - return - } - - def formatter = new SimpleDateFormat('yyyy-MM-dd') - writer.println() - writer.println("## [${next}](https://github.com/auth0/${repository}/tree/${next}) (${formatter.format(new Date())})") - writer.println("[Full Changelog](https://github.com/auth0/${repository}/compare/${current}...${next})") - def command = ["curl", "https://webtask.it.auth0.com/api/run/wt-hernan-auth0_com-0/oss-changelog.js?webtask_no_cache=1&repo=${repository}&milestone=${next}", "-f", "-s", "-H", "Accept: text/markdown"] - def content = command.execute() - content.consumeProcessOutputStream(writer) - if (content.waitFor() != 0) { - throw new GradleException("Failed to request changelog for version ${next}") - } - } - } - file.delete() - output.renameTo('CHANGELOG.md') - } -} - -class ReleaseTask extends DefaultTask { - - def tagName - - @TaskAction - def perform() { - def path = project.getRootProject().getProjectDir().path - project.exec { - commandLine 'git', 'add', 'README.md' - workingDir path - } - project.exec { - commandLine 'git', 'add', 'CHANGELOG.md' - workingDir path - } - project.exec { - commandLine 'git', 'commit', '-m', "Release ${tagName}" - workingDir path - } - project.exec { - commandLine 'git', 'tag', "${tagName}" - workingDir path - } - } -} - -class ReadmeTask extends DefaultTask { - - def current - def next - - final filename = 'README.md' - - @TaskAction - def update() { - def file = new File(filename) - def gradleUpdated = "compile '${project.group}:${project.name}:${next}'" - def oldSingleQuote = "compile '${project.group}:${project.name}:${current}'" - def oldDoubleQuote = "compile \"${project.group}:${project.name}:${current}\"" - def mavenUpdated = "${next}" - def mavenOld = "${current}" - def contents = file.getText('UTF-8') - contents = contents.replace(oldSingleQuote, gradleUpdated).replace(oldDoubleQuote, gradleUpdated).replace(mavenOld, mavenUpdated) - file.write(contents, 'UTF-8') - } - -} - -class ReleasePlugin implements Plugin { - void apply(Project target) { - def semver = current() - target.version = semver.stringVersion - def version = semver.version - def nextMinor = semver.nextMinor() - def nextPatch = semver.nextPatch() - target.task('changelogMinor', type: ChangeLogTask) { - current = version - next = nextMinor - } - target.task('changelogPatch', type: ChangeLogTask) { - current = version - next = nextPatch - } - target.task('readmeMinor', type: ReadmeTask, dependsOn: 'changelogMinor') { - current = version - next = nextMinor - } - target.task('readmePatch', type: ReadmeTask, dependsOn: 'changelogPatch') { - current = version - next = nextPatch - } - target.task('releaseMinor', type: ReleaseTask, dependsOn: 'readmeMinor') { - tagName = nextMinor - } - target.task('releasePatch', type: ReleaseTask, dependsOn: 'readmePatch') { - tagName = nextPatch - } - } - - static def current() { - def current = describeGit(false) - def snapshot = current == null - if (snapshot) { - current = describeGit(snapshot, '0.0.1') - } - return new Semver(snapshot: snapshot, version: current) - } - - static def describeGit(boolean snapshot, String defaultValue = null) { - def command = ['git', 'describe', '--tags', (snapshot ? '--abbrev=0' : '--exact-match')].execute() - def stdout = new ByteArrayOutputStream() - def errout = new ByteArrayOutputStream() - command.consumeProcessOutput(stdout, errout) - if (command.waitFor() != 0) { - Logging.getLogger(ReleasePlugin.class).debug(errout.toString()) - return defaultValue - } - if (stdout.toByteArray().length > 0) { - return stdout.toString().replace('\n', "") - } - - return defaultValue - } -} \ No newline at end of file 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')