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 870edff2..e706fbb1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,9 @@ Temporary Items ## Plugin-specific files: # IntelliJ +bin/ /out/ +/lib/out/ # mpeltonen/sbt-idea plugin .idea_modules/ @@ -55,6 +57,11 @@ crashlytics.properties crashlytics-build.properties fabric.properties +# Eclipse IDE +.classpath +.project +.settings/ + ### Java template *.class @@ -74,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/.travis.yml b/.travis.yml deleted file mode 100644 index afa24235..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: java -jdk: - - openjdk7 - - oraclejdk7 - - oraclejdk8 -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -sudo: false -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ -script: - - "./gradlew clean check jacocoTestReport --continue" -after_success: - - bash <(curl -s https://codecov.io/bash) -after_failure: - - cat $HOME/travis/build/auth0/java-jwt/lib/build/reports/tests/index.html -branches: - only: - - v3 \ 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 758b7b55..b97fab71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,459 @@ # 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** +- Wrong ES256 signature length [\#187](https://github.com/auth0/java-jwt/issues/187) + +**Fixed** +- Rework ECDSA [\#212](https://github.com/auth0/java-jwt/pull/212) ([lbalmaceda](https://github.com/lbalmaceda)) +- Instantiate exception only when required [\#198](https://github.com/auth0/java-jwt/pull/198) ([rumdidumdum](https://github.com/rumdidumdum)) + +## [3.2.0](https://github.com/auth0/java-jwt/tree/3.2.0) (2017-05-04) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.1.0...3.2.0) +**Closed issues** +- Claim.isNull() returns true for JSON Object constructed claims [\#160](https://github.com/auth0/java-jwt/issues/160) +- Incorrectly rejects whitespace after JSON header as invalid [\#144](https://github.com/auth0/java-jwt/issues/144) +- No token type [\#136](https://github.com/auth0/java-jwt/issues/136) +- Timestamps are limited by Integer/int to 2038-01-19T04:14:07.000+0100 [\#132](https://github.com/auth0/java-jwt/issues/132) + +**Added** +- Refactor KeyProvider to receive the "Key Id" [\#167](https://github.com/auth0/java-jwt/pull/167) ([lbalmaceda](https://github.com/lbalmaceda)) +- Add Sign/Verify of Long type claims [\#157](https://github.com/auth0/java-jwt/pull/157) ([vrancic](https://github.com/vrancic)) +- added date validation dedicated exception [\#155](https://github.com/auth0/java-jwt/pull/155) ([Spyna](https://github.com/Spyna)) +- Allow to get a Claim as Map [\#152](https://github.com/auth0/java-jwt/pull/152) ([lbalmaceda](https://github.com/lbalmaceda)) +- Add Algorithm KeyProvider interface [\#149](https://github.com/auth0/java-jwt/pull/149) ([lbalmaceda](https://github.com/lbalmaceda)) +- Instantiate RSA/EC Algorithm with both keys [\#147](https://github.com/auth0/java-jwt/pull/147) ([lbalmaceda](https://github.com/lbalmaceda)) +- Add Key Id setter and set JWT Type after signing [\#138](https://github.com/auth0/java-jwt/pull/138) ([lbalmaceda](https://github.com/lbalmaceda)) + +**Changed** +- Change the JWT.decode() return type to DecodedJWT [\#150](https://github.com/auth0/java-jwt/pull/150) ([lbalmaceda](https://github.com/lbalmaceda)) + +**Fixed** +- Fix Claim.isNull() method for JSON Objects [\#161](https://github.com/auth0/java-jwt/pull/161) ([lbalmaceda](https://github.com/lbalmaceda)) +- Accept blanks, new line and carriage returns on JSON [\#151](https://github.com/auth0/java-jwt/pull/151) ([lbalmaceda](https://github.com/lbalmaceda)) +- Fix Date value conversion [\#137](https://github.com/auth0/java-jwt/pull/137) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.1.0](https://github.com/auth0/java-jwt/tree/3.1.0) (2017-01-04) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.0.2...3.1.0) + +**Added** +- Make Clock customization accessible for verification [\#125](https://github.com/auth0/java-jwt/pull/125) ([lbalmaceda](https://github.com/lbalmaceda)) +- Add getter for all the Payload's Claims [\#124](https://github.com/auth0/java-jwt/pull/124) ([lbalmaceda](https://github.com/lbalmaceda)) +- Accept Array type on verification and creation. [\#123](https://github.com/auth0/java-jwt/pull/123) ([lbalmaceda](https://github.com/lbalmaceda)) + +## [3.0.2](https://github.com/auth0/java-jwt/tree/3.0.2) (2016-12-13) +[Full Changelog](https://github.com/auth0/java-jwt/compare/3.0.1...3.0.2) + +**Fixed** +- Add targetCompatibility to 1.7 [\#121](https://github.com/auth0/java-jwt/pull/121) ([hzalaz](https://github.com/hzalaz)) + ## [3.0.1](https://github.com/auth0/java-jwt/tree/3.0.0) (2016-12-05) [Full Changelog](https://github.com/auth0/java-jwt/compare/3.0.0...3.0.1) @@ -41,4 +495,4 @@ The library implements JWT Verification and Signing using the following algorith | RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 | | ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 | | ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 | -| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | \ No newline at end of file +| ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 | 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 new file mode 100644 index 00000000..bcd1854c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Auth0, Inc. (http://auth0.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +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. 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 42172e8d..9d0ae41c 100644 --- a/README.md +++ b/README.md @@ -1,34 +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) -[![Build Status](https://travis-ci.org/auth0/java-jwt.svg?branch=v3)](https://travis-ci.org/auth0/java-jwt) -[![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. -## Installation +## Getting Started -### Maven +### Requirements -```xml - - com.auth0 - java-jwt - 3.0.1 - -``` +This library is supported for Java LTS versions 8, 11, and 17. For issues on non-LTS versions above 8, consideration will be given on a case-by-case basis. -### Gradle +> `java-jwt` is intended for server-side JVM applications. Android applications should use [JWTDecode.Android](https://github.com/auth0/JWTDecode.Android). -```gradle -compile 'com.auth0:java-jwt:3.0.1' -``` - -## Available Algorithms - -The library implements JWT Verification and Signing using the following algorithms: +`java-jwt` supports the following algorithms for both signing and verification: | JWS | Algorithm | Description | | :-------------: | :-------------: | :----- | @@ -42,290 +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 +> 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) -### Decode a Token +> :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 -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -try { - JWT 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. +Add the dependency via Maven: +```xml + + com.auth0 + java-jwt + 4.5.0 + +``` -### Create and Sign a Token +or Gradle: -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. +```gradle +implementation 'com.auth0:java-jwt:4.5.0' +``` -* Example using `HS256` +### Create a JWT -```java -try { - String token = JWT.create() - .withIssuer("auth0") - .sign(Algorithm.HMAC256("secret")); -} catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. -} -``` +Use `JWT.create()`, configure the claims, and then call `sign(algorithm)` to sign the JWT. -* Example using `RS256` +The example below demonstrates this using the `RS256` signing algorithm: ```java -PrivateKey key = //Get the key instance try { + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); String token = JWT.create() .withIssuer("auth0") - .sign(Algorithm.RSA256(key)); + .sign(algorithm); } catch (JWTCreationException exception){ - //Invalid Signing configuration / Couldn't convert Claims. + // 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. +### Verify a JWT -* Example using `HS256` - -```java -String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -try { - JWTVerifier verifier = JWT.require(Algorithm.HMAC256("secret")) - .withIssuer("auth0") - .build(); //Reusable verifier instance - JWT jwt = verifier.verify(token); -} catch (JWTVerificationException exception){ - //Invalid signature/claims -} -``` +Create a `JWTVerifier` passing the `Algorithm`, and specify any required claim values. -* Example using `RS256` +The following example uses `RS256` to verify the JWT. ```java String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; -PublicKey key = //Get the key instance +DecodedJWT decodedJWT; try { - JWTVerifier verifier = JWT.require(Algorithm.RSA256(key)) + Algorithm algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey); + JWTVerifier verifier = JWT.require(algorithm) + // specify any specific claim validations .withIssuer("auth0") - .build(); //Reusable verifier instance - JWT jwt = verifier.verify(token); + // reusable verifier instance + .build(); + + decodedJWT = verifier.verify(token); } catch (JWTVerificationException exception){ - //Invalid signature/claims + // 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.RSA256(key)) - .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.RSA256(key)) - .acceptLeeway(1) //1 sec for nbf and iat - .acceptExpiresAt(5) //5 secs for exp - .build(); -``` - - -### 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"); -``` - - -### 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 `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 -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 -JWT.create() - .withClaim("name", 123) - .sign(Algorithm.HMAC256("secret")); -``` - -You can also verify custom Claims on the `JWT.require()` by calling `withClaim()` and passing both the name and the required value. - -```java -JWT.require(Algorithm.HMAC256("secret")) - .withClaim("name", 123) - .build() - .verify("my.jwt.token"); -``` - -> The value of the custom Claim in all the cases must be of a `Integer`, `Double`, `Date`, `String`, or `Boolean` class. - - -### 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. -* **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. - -#### Collections -To obtain a Claim as a Collection you'll need to provide the **Class Type** of the contents to convert from. - -* **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 inside the JSON Array 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](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 e6942415..83093fc1 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,45 +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' + 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.8.4' - compile 'commons-codec:commons-codec:1.10' - compile 'org.bouncycastle:bcprov-jdk15on:1.55' - testCompile 'junit:junit:4.12' - testCompile 'net.jodah:concurrentunit:0.4.2' - testCompile 'org.hamcrest:hamcrest-library:1.3' - testCompile 'org.mockito:mockito-core:2.2.8' + 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 { @@ -51,12 +149,96 @@ jacocoTestReport { test { testLogging { - events "passed", "skipped", "failed", "standardError" + events "skipped", "failed", "standardError" exceptionFormat "short" } } -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/Clock.java b/lib/src/main/java/com/auth0/jwt/Clock.java deleted file mode 100644 index b2f526cc..00000000 --- a/lib/src/main/java/com/auth0/jwt/Clock.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.auth0.jwt; - -import java.util.Date; - -/** - * The Clock class is used to wrap calls to Date class. - */ -class Clock { - - Clock() { - } - - /** - * Returns a new Date representing Today's time. - * - * @return a new Date representing Today's time. - */ - 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 88940fd3..696abe40 100644 --- a/lib/src/main/java/com/auth0/jwt/JWT.java +++ b/lib/src/main/java/com/auth0/jwt/JWT.java @@ -2,43 +2,71 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.impl.JWTParser; import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.Verification; -import java.util.Date; -import java.util.List; - +/** + * Exposes all the JWT functionalities. + */ @SuppressWarnings("WeakerAccess") -public abstract class JWT implements DecodedJWT { +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 JWT 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 if you have already verified it. * - * Note that this method doesn't verify the token's signature! Use it only if you trust the token or you already verified it. + * @param token with jwt format as string. + * @return a decoded JWT. + * @throws JWTDecodeException if any part of the token contained an invalid jwt + * or JSON format of each of the jwt parts. + */ + 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 if you have already verified it. * * @param token with jwt format as string. - * @return a decoded token. - * @throws JWTDecodeException if any part of the token contained an invalid jwt or JSON format of each of the jwt parts. + * @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 static JWT decode(String token) throws JWTDecodeException { + 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 JWTVerifier.Verification require(Algorithm algorithm) { + public static Verification require(Algorithm algorithm) { return JWTVerifier.init(algorithm); } /** - * Returns a JWT builder used to create and sign jwt tokens + * Returns a Json Web Token builder used to create and sign tokens. * - * @return a jwt token builder. + * @return a token builder. */ public static JWTCreator.Builder create() { return JWTCreator.init(); diff --git a/lib/src/main/java/com/auth0/jwt/JWTCreator.java b/lib/src/main/java/com/auth0/jwt/JWTCreator.java index 526a9000..bfcb9147 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTCreator.java +++ b/lib/src/main/java/com/auth0/jwt/JWTCreator.java @@ -3,21 +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 { @@ -26,15 +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); - 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); } @@ -55,156 +68,564 @@ 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. + * + * @param keyId the Key Id value. + * @return this same Builder instance. + */ + public Builder withKeyId(String keyId) { + this.headerClaims.put(HeaderParams.KEY_ID, keyId); return this; } /** - * Add a specific Issuer ("iss") claim. + * Add a specific Issuer ("iss") claim to the Payload. * * @param issuer the Issuer value. * @return this same Builder instance. */ public Builder withIssuer(String issuer) { - addClaim(PublicClaims.ISSUER, issuer); + addClaim(RegisteredClaims.ISSUER, issuer); return this; } /** - * Add a specific Subject ("sub") claim. + * Add a specific Subject ("sub") claim to the Payload. * * @param subject the Subject value. * @return this same Builder instance. */ public Builder withSubject(String subject) { - addClaim(PublicClaims.SUBJECT, subject); + addClaim(RegisteredClaims.SUBJECT, subject); return this; } /** - * Add a specific Audience ("aud") claim. + * Add a specific Audience ("aud") claim to the Payload. * * @param audience the Audience value. * @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. + * 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 Not Before ("nbf") claim. + * 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. 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. + * 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; } /** - * Add a specific JWT Id ("jti") claim. + * Add a specific JWT Id ("jti") claim to the Payload. * * @param jwtId the Token Id value. * @return this same Builder instance. */ public Builder withJWTId(String jwtId) { - addClaim(PublicClaims.JWT_ID, jwtId); + addClaim(RegisteredClaims.JWT_ID, jwtId); return this; } /** * Add a custom Claim value. * - * @param name the Claim's name - * @param value the Claim's value. Must be an instance of Integer, Double, Boolean, Date or String class. + * @param name the Claim's name. + * @param value the Claim's value. * @return this same Builder instance. - * @throws IllegalArgumentException if the name is null or the value class is not allowed. + * @throws IllegalArgumentException if the name is null. */ - public Builder withClaim(String name, Object value) throws IllegalArgumentException { - final boolean validValue = value instanceof Integer || value instanceof Double || - value instanceof Boolean || value instanceof Date || value instanceof String; - if (name == null) { - throw new IllegalArgumentException("The Custom Claim's name can't be null."); + public Builder withClaim(String name, Boolean value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + return this; + } + + /** + * Add a custom Claim value. + * + * @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, Integer value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + return this; + } + + /** + * Add a custom Claim value. + * + * @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, Long value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + return this; + } + + /** + * Add a custom Claim value. + * + * @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, Double value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + return this; + } + + /** + * Add a custom Claim value. + * + * @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, String value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + 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, Date value) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, value); + 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"); } - if (!validValue) { - throw new IllegalArgumentException("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String."); + 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; + } - addClaim(name, value); + /** + * 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; } /** - * Creates a new JWT and signs is with the given algorithm + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withArrayClaim(String name, String[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + + /** + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null. + */ + public Builder withArrayClaim(String name, Integer[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + + /** + * Add a custom Array Claim with the given items. + * + * @param name the Claim's name. + * @param items the Claim's value. + * @return this same Builder instance. + * @throws IllegalArgumentException if the name is null + */ + public Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException { + assertNonNull(name); + addClaim(name, items); + return this; + } + + /** + * Add specific Claims to set as the Payload. If the provided map is null then + * nothing is changed. + *

+ * 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(HeaderParams.ALGORITHM, algorithm.getName()); + if (!headerClaims.containsKey(HeaderParams.TYPE)) { + headerClaims.put(HeaderParams.TYPE, "JWT"); + } + String signingKeyId = algorithm.getSigningKeyId(); + if (signingKeyId != null) { + withKeyId(signingKeyId); + } return new JWTCreator(algorithm, headerClaims, payloadClaims).sign(); } - private void addClaim(String name, Object value) { - if (value == null) { - payloadClaims.remove(name); - return; + private void assertNonNull(String name) { + if (name == null) { + throw new IllegalArgumentException("The Custom Claim's name can't be null."); } + } + + private void addClaim(String name, Object value) { 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 1c0c0afc..cc283095 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTDecoder.java +++ b/lib/src/main/java/com/auth0/jwt/JWTDecoder.java @@ -6,42 +6,47 @@ 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 extends JWT { +final class JWTDecoder implements DecodedJWT, Serializable { - private final String token; - private Header header; - private Payload payload; - private String signature; + private static final long serialVersionUID = 1873362438023312895L; + + private final String[] parts; + private final Header header; + private final Payload payload; JWTDecoder(String jwt) throws JWTDecodeException { - this.token = jwt; - parseToken(jwt); + this(new JWTParser(), jwt); } - private void parseToken(String token) throws JWTDecodeException { - final String[] parts = TokenUtils.splitToken(token); - final JWTParser converter = new JWTParser(); + JWTDecoder(JWTParser converter, String jwt) throws JWTDecodeException { + parts = TokenUtils.splitToken(jwt); 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); - signature = parts[2]; } @Override @@ -89,16 +94,31 @@ public Date getExpiresAt() { return payload.getExpiresAt(); } + @Override + public Instant getExpiresAtAsInstant() { + return payload.getExpiresAtAsInstant(); + } + @Override public Date getNotBefore() { return payload.getNotBefore(); } + @Override + public Instant getNotBeforeAsInstant() { + return payload.getNotBeforeAsInstant(); + } + @Override public Date getIssuedAt() { return payload.getIssuedAt(); } + @Override + public Instant getIssuedAtAsInstant() { + return payload.getIssuedAtAsInstant(); + } + @Override public String getId() { return payload.getId(); @@ -109,13 +129,28 @@ public Claim getClaim(String name) { return payload.getClaim(name); } + @Override + public Map getClaims() { + return payload.getClaims(); + } + + @Override + public String getHeader() { + return parts[0]; + } + + @Override + public String getPayload() { + return parts[1]; + } + @Override public String getSignature() { - return signature; + return parts[2]; } @Override public String getToken() { - return token; + return String.format("%s.%s.%s", parts[0], parts[1], parts[2]); } } diff --git a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java index b9077cb2..bf180300 100644 --- a/lib/src/main/java/com/auth0/jwt/JWTVerifier.java +++ b/lib/src/main/java/com/auth0/jwt/JWTVerifier.java @@ -1,199 +1,279 @@ 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.JWTVerificationException; -import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.impl.PublicClaims; +import com.auth0.jwt.exceptions.*; +import com.auth0.jwt.impl.JWTParser; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; -import org.apache.commons.codec.binary.Base64; +import com.auth0.jwt.impl.ExpectedCheckHolder; +import com.auth0.jwt.interfaces.Verification; -import java.nio.charset.StandardCharsets; +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 JWTVerifier.Verification init(Algorithm algorithm) throws IllegalArgumentException { - return new Verification(algorithm); + static Verification init(Algorithm algorithm) throws IllegalArgumentException { + return new BaseVerification(algorithm); } /** - * 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 Verification { + 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; - Verification(Algorithm algorithm) throws IllegalArgumentException { + BaseVerification(Algorithm algorithm) throws IllegalArgumentException { if (algorithm == null) { throw new IllegalArgumentException("The Algorithm cannot be null."); } 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. - */ - public Verification withIssuer(String issuer) { - requireClaim(PublicClaims.ISSUER, issuer); + @Override + 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; } - /** - * Define the default window in milliseconds 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 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; + } + + @Override public Verification acceptLeeway(long leeway) throws IllegalArgumentException { - if (leeway < 0) { - throw new IllegalArgumentException("Leeway value can't be negative."); - } + assertPositive(leeway); this.defaultLeeway = leeway; 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 { - if (leeway < 0) { - throw new IllegalArgumentException("Leeway value can't be negative."); - } - requireClaim(PublicClaims.EXPIRES_AT, leeway); + assertPositive(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 { - if (leeway < 0) { - throw new IllegalArgumentException("Leeway value can't be negative."); - } - requireClaim(PublicClaims.NOT_BEFORE, leeway); + assertPositive(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 { - if (leeway < 0) { - throw new IllegalArgumentException("Leeway value can't be negative."); - } - requireClaim(PublicClaims.ISSUED_AT, leeway); + assertPositive(leeway); + customLeeways.put(RegisteredClaims.ISSUED_AT, leeway); return this; } - /** - * Require a specific JWT Id ("jti") claim. - * - * @param jwtId the required Id value - * @return this same Verification instance. - */ + @Override + public Verification ignoreIssuedAt() { + this.ignoreIssuedAt = true; + return this; + } + + @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; } - /** - * Require a specific Claim value. - * - * @param name the Claim's name - * @param value the Claim's value. Must be an instance of Integer, Double, Boolean, Date or String class. - * @return this same Verification instance. - * @throws IllegalArgumentException if the name is null or the value class is not allowed. - */ - public Verification withClaim(String name, Object value) throws IllegalArgumentException { - final boolean validValue = value instanceof Integer || value instanceof Double || - value instanceof Boolean || value instanceof Date || value instanceof String; - if (name == null) { - throw new IllegalArgumentException("The Custom Claim's name can't be null."); - } - if (!validValue) { - throw new IllegalArgumentException("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String."); - } + @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; + } - requireClaim(name, value); + @Override + public Verification withNullClaim(String name) throws IllegalArgumentException { + assertNonNull(name); + withClaim(name, ((claim, decodedJWT) -> claim.isNull())); return this; } - /** - * Creates a new and reusable instance of the JWTVerifier with the configuration already provided. - * - * @return a new JWTVerifier instance. - */ + @Override + public Verification withClaim(String name, Boolean value) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asBoolean()))); + return this; + } + + @Override + public Verification withClaim(String name, Integer value) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asInt()))); + return this; + } + + @Override + public Verification withClaim(String name, Long value) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asLong()))); + return this; + } + + @Override + public Verification withClaim(String name, Double value) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asDouble()))); + return this; + } + + @Override + public Verification withClaim(String name, String value) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, value) + || value.equals(claim.asString()))); + return this; + } + + @Override + public Verification withClaim(String name, Date value) throws IllegalArgumentException { + return withClaim(name, value != null ? value.toInstant() : null); + } + + @Override + public Verification withClaim(String name, Instant value) throws IllegalArgumentException { + assertNonNull(name); + // Since date-time claims are serialized as epoch seconds, + // we need to compare them with only seconds-granularity + addCheck(name, + ((claim, decodedJWT) -> verifyNull(claim, value) + || value.truncatedTo(ChronoUnit.SECONDS).equals(claim.asInstant()))); + return this; + } + + @Override + public Verification withClaim(String name, BiPredicate predicate) + throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, predicate) + || predicate.test(claim, decodedJWT))); + return this; + } + + @Override + public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException { + assertNonNull(name); + addCheck(name, ((claim, decodedJWT) -> verifyNull(claim, items) + || assertValidCollectionClaim(claim, items))); + return this; + } + + @Override + public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException { + assertNonNull(name); + 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; + } + + @Override public JWTVerifier build() { - return this.build(new Clock()); + return this.build(Clock.systemUTC()); } /** @@ -201,31 +281,155 @@ 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) { + 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 */ - JWTVerifier build(Clock clock) { - addLeewayToDateClaims(); - return new JWTVerifier(algorithm, claims, clock); + public long getLeewayFor(String name) { + return customLeeways.getOrDefault(name, defaultLeeway); } - private void addLeewayToDateClaims() { - if (!claims.containsKey(PublicClaims.EXPIRES_AT)) { - claims.put(PublicClaims.EXPIRES_AT, 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))); } - if (!claims.containsKey(PublicClaims.NOT_BEFORE)) { - claims.put(PublicClaims.NOT_BEFORE, defaultLeeway); + } + + 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)); } - if (!claims.containsKey(PublicClaims.ISSUED_AT)) { - claims.put(PublicClaims.ISSUED_AT, defaultLeeway); + 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 void requireClaim(String name, Object value) { - if (value == null) { - claims.remove(name); - return; + 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); } - claims.put(name, value); + } + + private void assertPositive(long leeway) { + if (leeway < 0) { + throw new IllegalArgumentException("Leeway value can't be negative."); + } + } + + private void assertNonNull(String name) { + if (name == null) { + throw new IllegalArgumentException("The Custom Claim's name can't be null."); + } + } + + private void addCheck(String name, BiPredicate predicate) { + expectedChecks.add(constructExpectedCheck(name, (claim, decodedJWT) -> { + if (claim.isMissing()) { + throw new MissingClaimException(name); + } + return predicate.test(claim, decodedJWT); + })); + } + + private ExpectedCheckHolder constructExpectedCheck(String claimName, BiPredicate check) { + return new ExpectedCheckHolder() { + @Override + public String getClaimName() { + return claimName; + } + + @Override + public boolean verify(Claim claim, DecodedJWT decodedJWT) { + return check.test(claim, decodedJWT); + } + }; + } + + private boolean verifyNull(Claim claim, Object value) { + return value == null && claim.isNull(); + } + + private boolean isNullOrEmpty(String[] args) { + if (args == null || args.length == 0) { + return true; + } + boolean isAllNull = true; + for (String arg : args) { + if (arg != null) { + isAllNull = false; + break; + } + } + return isAllNull; } } @@ -235,107 +439,62 @@ private void requireClaim(String name, Object value) { * * @param token to verify. * @return a verified and decoded JWT. - * @throws JWTVerificationException if any of the required contents inside the JWT is invalid. + * @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(String token) throws JWTVerificationException { - DecodedJWT jwt = JWTDecoder.decode(token); - verifyAlgorithm(jwt, algorithm); - verifySignature(TokenUtils.splitToken(token)); - verifyClaims(jwt, claims); - return jwt; + DecodedJWT jwt = new JWTDecoder(parser, token); + return verify(jwt); } - private void verifySignature(String[] parts) throws SignatureVerificationException { - byte[] content = String.format("%s.%s", parts[0], parts[1]).getBytes(StandardCharsets.UTF_8); - byte[] signature = Base64.decodeBase64(parts[2]); - algorithm.verify(content, signature); + /** + * 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, 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) { - 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 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()); - } - - 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 - boolean isValid; - String errMessage; - if (shouldBeFuture) { - today.setTime(today.getTime() - leeway * 1000); - isValid = date == null || !today.after(date); - errMessage = String.format("The Token has expired on %s.", date); - } else { - today.setTime(today.getTime() + leeway * 1000); - isValid = date == null || !today.before(date); - errMessage = String.format("The Token can't be used before %s.", date); - } - if (!isValid) { - throw new InvalidClaimException(errMessage); - } - } - - 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 b74b1aa3..248af7c5 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/Algorithm.java @@ -2,14 +2,18 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.RSAKeyProvider; -import java.io.UnsupportedEncodingException; -import java.security.interfaces.ECKey; -import java.security.interfaces.RSAKey; +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 { private final String name; @@ -18,12 +22,60 @@ public abstract class Algorithm { /** * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256". * - * @param key the key to use in the verify or signing instance. + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. * @return a valid RSA256 Algorithm. * @throws IllegalArgumentException if the provided Key is null. */ + public static Algorithm RSA256(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("RS256", "SHA256withRSA", keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSA256 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. + */ + public static Algorithm RSA256(RSAPublicKey publicKey, RSAPrivateKey privateKey) throws IllegalArgumentException { + return RSA256(RSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + + /** + * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256". + * + * @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. + */ public static Algorithm RSA256(RSAKey key) throws IllegalArgumentException { - return new RSAAlgorithm("RS256", "SHA256withRSA", key); + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA256(publicKey, privateKey); + } + + /** + * Creates a new Algorithm instance using SHA384withRSA. Tokens specify this as "RS384". + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid RSA384 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm RSA384(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("RS384", "SHA384withRSA", keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA384withRSA. Tokens specify this as "RS384". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSA384 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. + */ + public static Algorithm RSA384(RSAPublicKey publicKey, RSAPrivateKey privateKey) throws IllegalArgumentException { + return RSA384(RSAAlgorithm.providerForKeys(publicKey, privateKey)); } /** @@ -34,60 +86,64 @@ public static Algorithm RSA256(RSAKey key) throws IllegalArgumentException { * @throws IllegalArgumentException if the provided Key is null. */ public static Algorithm RSA384(RSAKey key) throws IllegalArgumentException { - return new RSAAlgorithm("RS384", "SHA384withRSA", key); + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA384(publicKey, privateKey); } /** * Creates a new Algorithm instance using SHA512withRSA. Tokens specify this as "RS512". * - * @param key the key to use in the verify or signing instance. + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. * @return a valid RSA512 Algorithm. - * @throws IllegalArgumentException if the provided Key is null. + * @throws IllegalArgumentException if the Key Provider is null. */ - public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { - return new RSAAlgorithm("RS512", "SHA512withRSA", key); + public static Algorithm RSA512(RSAKeyProvider keyProvider) throws IllegalArgumentException { + return new RSAAlgorithm("RS512", "SHA512withRSA", keyProvider); } /** - * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". + * Creates a new Algorithm instance using SHA512withRSA. Tokens specify this as "RS512". * - * @param secret the secret to use in the verify or signing instance. - * @return a valid HMAC256 Algorithm. - * @throws IllegalArgumentException if the provided Secret is null. - * @throws UnsupportedEncodingException if the current Java platform implementation doesn't support the UTF-8 character encoding. + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid RSA512 Algorithm. + * @throws IllegalArgumentException if both provided Keys are null. */ - public static Algorithm HMAC256(String secret) throws IllegalArgumentException, UnsupportedEncodingException { - return new HMACAlgorithm("HS256", "HmacSHA256", secret); + public static Algorithm RSA512(RSAPublicKey publicKey, RSAPrivateKey privateKey) throws IllegalArgumentException { + return RSA512(RSAAlgorithm.providerForKeys(publicKey, privateKey)); } /** - * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". + * Creates a new Algorithm instance using SHA512withRSA. Tokens specify this as "RS512". * - * @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 key the key to use in the verify or signing instance. + * @return a valid RSA512 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. */ - public static Algorithm HMAC384(String secret) throws IllegalArgumentException, UnsupportedEncodingException { - return new HMACAlgorithm("HS384", "HmacSHA384", secret); + public static Algorithm RSA512(RSAKey key) throws IllegalArgumentException { + RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null; + RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null; + return RSA512(publicKey, privateKey); } /** - * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". + * 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 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 256 bit long + * @return a valid HMAC256 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 HMAC256(String secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS256", "HmacSHA256", secret); } /** * Creates a new Algorithm instance using HmacSHA256. Tokens specify this as "HS256". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 256 bit long * @return a valid HMAC256 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -99,6 +155,19 @@ public static Algorithm HMAC256(byte[] secret) throws IllegalArgumentException { * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 384 bit long + * @return a valid HMAC384 Algorithm. + * @throws IllegalArgumentException if the provided Secret is null. + */ + public static Algorithm HMAC384(String secret) throws IllegalArgumentException { + return new HMACAlgorithm("HS384", "HmacSHA384", secret); + } + + /** + * Creates a new Algorithm instance using HmacSHA384. Tokens specify this as "HS384". + * + * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 384 bit long * @return a valid HMAC384 Algorithm. * @throws IllegalArgumentException if the provided Secret is null. */ @@ -110,6 +179,19 @@ public static Algorithm HMAC384(byte[] secret) throws IllegalArgumentException { * Creates a new Algorithm instance using HmacSHA512. Tokens specify this as "HS512". * * @param secret the secret bytes to use in the verify or signing instance. + * Ensure the length of the secret is at least 512 bit long + * @return a valid HMAC512 Algorithm. + * @throws IllegalArgumentException if the provided Secret is null. + */ + 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. */ @@ -117,6 +199,30 @@ 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". + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid ECDSA256 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm ECDSA256(ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + return new ECDSAAlgorithm("ES256", "SHA256withECDSA", 32, keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid ECDSA256 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm ECDSA256(ECPublicKey publicKey, ECPrivateKey privateKey) throws IllegalArgumentException { + return ECDSA256(ECDSAAlgorithm.providerForKeys(publicKey, privateKey)); + } + /** * Creates a new Algorithm instance using SHA256withECDSA. Tokens specify this as "ES256". * @@ -125,7 +231,32 @@ public static Algorithm HMAC512(byte[] secret) throws IllegalArgumentException { * @throws IllegalArgumentException if the provided Key is null. */ public static Algorithm ECDSA256(ECKey key) throws IllegalArgumentException { - return new ECDSAAlgorithm("ES256", "SHA256withECDSA", 32, key); + ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; + ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; + return ECDSA256(publicKey, privateKey); + } + + /** + * Creates a new Algorithm instance using SHA384withECDSA. Tokens specify this as "ES384". + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid ECDSA384 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm ECDSA384(ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + return new ECDSAAlgorithm("ES384", "SHA384withECDSA", 48, keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA384withECDSA. Tokens specify this as "ES384". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid ECDSA384 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm ECDSA384(ECPublicKey publicKey, ECPrivateKey privateKey) throws IllegalArgumentException { + return ECDSA384(ECDSAAlgorithm.providerForKeys(publicKey, privateKey)); } /** @@ -136,7 +267,32 @@ public static Algorithm ECDSA256(ECKey key) throws IllegalArgumentException { * @throws IllegalArgumentException if the provided Key is null. */ public static Algorithm ECDSA384(ECKey key) throws IllegalArgumentException { - return new ECDSAAlgorithm("ES384", "SHA384withECDSA", 48, key); + ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; + ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; + return ECDSA384(publicKey, privateKey); + } + + /** + * Creates a new Algorithm instance using SHA512withECDSA. Tokens specify this as "ES512". + * + * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance. + * @return a valid ECDSA512 Algorithm. + * @throws IllegalArgumentException if the Key Provider is null. + */ + public static Algorithm ECDSA512(ECDSAKeyProvider keyProvider) throws IllegalArgumentException { + return new ECDSAAlgorithm("ES512", "SHA512withECDSA", 66, keyProvider); + } + + /** + * Creates a new Algorithm instance using SHA512withECDSA. Tokens specify this as "ES512". + * + * @param publicKey the key to use in the verify instance. + * @param privateKey the key to use in the signing instance. + * @return a valid ECDSA512 Algorithm. + * @throws IllegalArgumentException if the provided Key is null. + */ + public static Algorithm ECDSA512(ECPublicKey publicKey, ECPrivateKey privateKey) throws IllegalArgumentException { + return ECDSA512(ECDSAAlgorithm.providerForKeys(publicKey, privateKey)); } /** @@ -147,9 +303,12 @@ public static Algorithm ECDSA384(ECKey key) throws IllegalArgumentException { * @throws IllegalArgumentException if the provided Key is null. */ public static Algorithm ECDSA512(ECKey key) throws IllegalArgumentException { - return new ECDSAAlgorithm("ES512", "SHA512withECDSA", 66, key); + ECPublicKey publicKey = key instanceof ECPublicKey ? (ECPublicKey) key : null; + ECPrivateKey privateKey = key instanceof ECPrivateKey ? (ECPrivateKey) key : null; + return ECDSA512(publicKey, privateKey); } + public static Algorithm none() { return new NoneAlgorithm(); } @@ -159,6 +318,16 @@ protected Algorithm(String name, String description) { this.description = 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. + * + * @return the Key Id that identifies the Signing Key or null if it's not specified. + */ + public String getSigningKeyId() { + return null; + } + /** * Getter for the name of this Algorithm, as defined in the JWT Standard. i.e. "HS256" * @@ -169,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. */ @@ -183,20 +353,46 @@ public String toString() { } /** - * Verify the given content using this Algorithm instance. + * Verify the given token using this Algorithm instance. * - * @param contentBytes an array of bytes representing the base64 encoded content to be verified against the signature. - * @param signatureBytes an array of bytes representing the base64 encoded signature to compare the content against. - * @throws SignatureVerificationException if the Token's Signature is invalid, meaning that it doesn't match the signatureBytes, or if the Key is invalid. + * @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. */ - public abstract void verify(byte[] contentBytes, byte[] signatureBytes) throws SignatureVerificationException; + 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 e24bf098..b3046097 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java @@ -2,82 +2,185 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.security.SignatureException; -import java.security.interfaces.ECKey; 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; private final CryptoHelper crypto; private final int ecNumberSize; - private final ECKey key; - ECDSAAlgorithm(CryptoHelper crypto, String id, String algorithm, int ecNumberSize, ECKey key) throws IllegalArgumentException { + //Visible for testing + ECDSAAlgorithm(CryptoHelper crypto, String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) + throws IllegalArgumentException { super(id, algorithm); - if (key == null) { - throw new IllegalArgumentException("The ECKey cannot be null"); + if (keyProvider == null) { + throw new IllegalArgumentException("The Key Provider cannot be null."); } - this.ecNumberSize = ecNumberSize; - this.key = key; + this.keyProvider = keyProvider; this.crypto = crypto; + this.ecNumberSize = ecNumberSize; } - ECDSAAlgorithm(String id, String algorithm, int ecNumberSize, ECKey key) throws IllegalArgumentException { - this(new CryptoHelper(), id, algorithm, ecNumberSize, key); - } - - ECKey getKey() { - return key; + ECDSAAlgorithm(String id, String algorithm, int ecNumberSize, ECDSAKeyProvider keyProvider) + throws IllegalArgumentException { + this(new CryptoHelper(), id, algorithm, ecNumberSize, keyProvider); } @Override - public void verify(byte[] contentBytes, byte[] signatureBytes) throws SignatureVerificationException { + public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - if (!(key instanceof ECPublicKey)) { - throw new IllegalArgumentException("The given ECKey is not an ECPublicKey."); - } - if (!isDERSignature(signatureBytes)) { - signatureBytes = JOSEToDER(signatureBytes); + 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(), (ECPublicKey) key, contentBytes, 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 | IllegalArgumentException 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 { - if (!(key instanceof ECPrivateKey)) { - throw new IllegalArgumentException("The given ECKey is not a ECPrivateKey."); + ECPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); } - return crypto.createSignatureFor(getDescription(), (PrivateKey) key, contentBytes); - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException e) { + byte[] signature = crypto.createSignatureFor(getDescription(), privateKey, contentBytes); + return DERToJOSE(signature); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { throw new SignatureGenerationException(this, e); } } - private boolean isDERSignature(byte[] signature) { + @Override + public String getSigningKeyId() { + return keyProvider.getPrivateKeyId(); + } + + //Visible for testing + byte[] DERToJOSE(byte[] derSignature) throws SignatureException { // DER Structure: http://crypto.stackexchange.com/a/1797 - // Should begin with 0x30 and have exactly the expected length - return signature[0] == 0x30 && signature.length != ecNumberSize * 2; + boolean derEncoded = derSignature[0] == 0x30 && derSignature.length != ecNumberSize * 2; + if (!derEncoded) { + throw new SignatureException("Invalid DER signature format."); + } + + final byte[] joseSignature = new byte[ecNumberSize * 2]; + + //Skip 0x30 + int offset = 1; + if (derSignature[1] == (byte) 0x81) { + //Skip sign + offset++; + } + + //Convert to unsigned. Should match DER length - offset + int encodedLength = derSignature[offset++] & 0xff; + if (encodedLength != derSignature.length - offset) { + throw new SignatureException("Invalid DER signature format."); + } + + //Skip 0x02 + offset++; + + //Obtain R number length (Includes padding) and skip it + int rlength = derSignature[offset++]; + if (rlength > ecNumberSize + 1) { + throw new SignatureException("Invalid DER signature format."); + } + 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)); + + //Skip R number and 0x02 + offset += rlength + 1; + + //Obtain S number length. (Includes padding) + int slength = derSignature[offset++]; + if (slength > ecNumberSize + 1) { + throw new SignatureException("Invalid DER signature format."); + } + 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)); + + return joseSignature; } - private 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(String.format("The signature length was invalid. Expected %d bytes but received %d", ecNumberSize * 2, joseSignature.length)); + 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; @@ -85,10 +188,34 @@ private byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { int length = 2 + rLength + 2 + sLength; if (length > 255) { - throw new SignatureException("Invalid ECDSA signature format"); + 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."); } - byte[] derSignature; + 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) { derSignature = new byte[3 + length]; @@ -100,31 +227,80 @@ private byte[] JOSEToDER(byte[] joseSignature) throws SignatureException { } // DER Structure: http://crypto.stackexchange.com/a/1797 - // Header with length info + // Header with signature length info derSignature[0] = (byte) 0x30; - derSignature[offset++] = (byte) length; + derSignature[offset++] = (byte) (length & 0xff); + + // Header with "min R" number length derSignature[offset++] = (byte) 0x02; derSignature[offset++] = (byte) rLength; // R number - System.arraycopy(joseSignature, 0, derSignature, offset + (rLength - ecNumberSize), ecNumberSize); - offset += rLength; + if (rPadding < 0) { + //Sign + derSignature[offset++] = (byte) 0x00; + System.arraycopy(joseSignature, 0, derSignature, offset, ecNumberSize); + offset += ecNumberSize; + } else { + int copyLength = Math.min(ecNumberSize, rLength); + System.arraycopy(joseSignature, rPadding, derSignature, offset, copyLength); + offset += copyLength; + } - // S number length + // Header with "min S" number length derSignature[offset++] = (byte) 0x02; derSignature[offset++] = (byte) sLength; // S number - System.arraycopy(joseSignature, ecNumberSize, derSignature, offset + (sLength - ecNumberSize), ecNumberSize); + if (sPadding < 0) { + //Sign + derSignature[offset++] = (byte) 0x00; + System.arraycopy(joseSignature, ecNumberSize, derSignature, offset, ecNumberSize); + } else { + System.arraycopy(joseSignature, ecNumberSize + sPadding, derSignature, offset, + Math.min(ecNumberSize, sLength)); + } 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) { padding++; } - return bytes[fromIndex + padding] > 0x7f ? padding : padding - 1; + return (bytes[fromIndex + padding] & 0xff) > 0x7f ? padding - 1 : padding; + } + + //Visible for testing + static ECDSAKeyProvider providerForKeys(final ECPublicKey publicKey, final ECPrivateKey privateKey) { + if (publicKey == null && privateKey == null) { + throw new IllegalArgumentException("Both provided Keys cannot be null."); + } + return new ECDSAKeyProvider() { + @Override + public ECPublicKey getPublicKeyById(String keyId) { + return publicKey; + } + + @Override + public ECPrivateKey getPrivateKey() { + return privateKey; + } + + @Override + public String getPrivateKeyId() { + return null; + } + }; } } 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 63f2d580..0306e7c4 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/HMACAlgorithm.java @@ -2,23 +2,32 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; -import org.apache.commons.codec.CharEncoding; +import com.auth0.jwt.interfaces.DecodedJWT; -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; - HMACAlgorithm(CryptoHelper crypto, String id, String algorithm, byte[] secretBytes) throws IllegalArgumentException { + //Visible for testing + 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; } @@ -26,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)); } - static byte[] getSecretBytes(String secret) throws IllegalArgumentException, UnsupportedEncodingException { + //Visible for testing + static byte[] getSecretBytes(String secret) throws IllegalArgumentException { if (secret == null) { throw new IllegalArgumentException("The Secret cannot be null"); } - return secret.getBytes(CharEncoding.UTF_8); - } - - byte[] getSecret() { - return secret; + return secret.getBytes(StandardCharsets.UTF_8); } @Override - public void verify(byte[] contentBytes, byte[] signatureBytes) throws SignatureVerificationException { + public void verify(DecodedJWT jwt) throws SignatureVerificationException { 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 { @@ -61,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 97b55edc..5c6c0fc5 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/NoneAlgorithm.java @@ -2,6 +2,8 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import java.util.Base64; class NoneAlgorithm extends Algorithm { @@ -10,12 +12,23 @@ class NoneAlgorithm extends Algorithm { } @Override - public void verify(byte[] contentBytes, byte[] signatureBytes) throws SignatureVerificationException { - if (signatureBytes.length > 0) { - throw new SignatureVerificationException(this); + public void verify(DecodedJWT jwt) throws SignatureVerificationException { + 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 cc217515..ca892e60 100644 --- a/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java +++ b/lib/src/main/java/com/auth0/jwt/algorithms/RSAAlgorithm.java @@ -2,58 +2,111 @@ import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.RSAKeyProvider; -import java.security.*; -import java.security.interfaces.RSAKey; +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 RSAKey key; + private final RSAKeyProvider keyProvider; private final CryptoHelper crypto; - RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKey key) throws IllegalArgumentException { + //Visible for testing + RSAAlgorithm(CryptoHelper crypto, String id, String algorithm, RSAKeyProvider keyProvider) + throws IllegalArgumentException { super(id, algorithm); - if (key == null) { - throw new IllegalArgumentException("The RSAKey cannot be null"); + if (keyProvider == null) { + throw new IllegalArgumentException("The Key Provider cannot be null."); } - this.key = key; + this.keyProvider = keyProvider; this.crypto = crypto; } - RSAAlgorithm(String id, String algorithm, RSAKey key) throws IllegalArgumentException { - this(new CryptoHelper(), id, algorithm, key); - } - - RSAKey getKey() { - return key; + RSAAlgorithm(String id, String algorithm, RSAKeyProvider keyProvider) throws IllegalArgumentException { + this(new CryptoHelper(), id, algorithm, keyProvider); } @Override - public void verify(byte[] contentBytes, byte[] signatureBytes) throws SignatureVerificationException { + public void verify(DecodedJWT jwt) throws SignatureVerificationException { try { - if (!(key instanceof PublicKey)) { - throw new IllegalArgumentException("The given RSAKey is not a RSAPublicKey."); + 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(), (RSAPublicKey) key, contentBytes, signatureBytes); + boolean valid = crypto.verifySignatureFor( + getDescription(), publicKey, jwt.getHeader(), jwt.getPayload(), signatureBytes); if (!valid) { throw new SignatureVerificationException(this); } - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException 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 { - if (!(key instanceof PrivateKey)) { - throw new IllegalArgumentException("The given RSAKey is not a RSAPrivateKey."); + RSAPrivateKey privateKey = keyProvider.getPrivateKey(); + if (privateKey == null) { + throw new IllegalStateException("The given Private Key is null."); } - return crypto.createSignatureFor(getDescription(), (RSAPrivateKey) key, contentBytes); - } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalArgumentException e) { + return crypto.createSignatureFor(getDescription(), privateKey, contentBytes); + } catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException | IllegalStateException e) { throw new SignatureGenerationException(this, e); } } + + @Override + public String getSigningKeyId() { + return keyProvider.getPrivateKeyId(); + } + + //Visible for testing + static RSAKeyProvider providerForKeys(final RSAPublicKey publicKey, final RSAPrivateKey privateKey) { + if (publicKey == null && privateKey == null) { + throw new IllegalArgumentException("Both provided Keys cannot be null."); + } + return new RSAKeyProvider() { + @Override + public RSAPublicKey getPublicKeyById(String keyId) { + return publicKey; + } + + @Override + public RSAPrivateKey getPrivateKey() { + return privateKey; + } + + @Override + public String getPrivateKeyId() { + return null; + } + }; + } } 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 new file mode 100644 index 00000000..42ab090d --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/exceptions/TokenExpiredException.java @@ -0,0 +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; + + 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 b633e1b2..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,22 +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 { - JWTDecodeException exception = new JWTDecodeException(String.format("The string '%s' doesn't have a valid JSON format.", json)); - if (json == null || !json.startsWith("{") || !json.endsWith("}")) { - throw exception; - } - try { - return mapper.readValue(json, tClazz); - } catch (IOException e) { - throw exception; - } + private static JWTDecodeException decodeException() { + return decodeException(null); + } + + 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 287f1270..0a7e22f3 100644 --- a/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java +++ b/lib/src/main/java/com/auth0/jwt/impl/JsonNodeClaim.java @@ -2,11 +2,15 @@ import com.auth0.jwt.exceptions.JWTDecodeException; 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; @@ -17,81 +21,138 @@ */ 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 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; } - ObjectMapper mapper = new ObjectMapper(); - 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] = mapper.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; } - ObjectMapper mapper = new ObjectMapper(); List list = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { try { - list.add(mapper.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; } + @Override + public Map asMap() throws JWTDecodeException { + if (isMissing() || isNull() || !data.isObject()) { + return null; + } + + 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 clazz) throws JWTDecodeException { + try { + 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 !(data.isArray() || data.canConvertToLong() || data.isTextual() || data.isNumber() || data.isBoolean()); + 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(); } /** @@ -99,23 +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); } -} + +} \ 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 3efb821e..00000000 --- a/lib/src/main/java/com/auth0/jwt/impl/NullClaim.java +++ /dev/null @@ -1,52 +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; - -/** - * 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 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; - } -} 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 a4998942..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,7 +115,15 @@ 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<>(tree.size() * 2); + for (String name : tree.keySet()) { + 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 2fc9c617..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 int dateToSeconds(Date date) { - return (int) (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 594ce254..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,19 +2,35 @@ import com.auth0.jwt.exceptions.JWTDecodeException; +import java.time.Instant; import java.util.Date; import java.util.List; +import java.util.Map; /** * The Claim class holds the value in a generic way so that it can be recovered in many representations. */ 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. */ @@ -22,15 +38,23 @@ 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. */ Integer asInt(); + /** + * Get this Claim as an Long. + * 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. + */ + Long asLong(); + /** * 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. */ @@ -38,35 +62,70 @@ 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, an empty Array will be returned. + * If the value isn't an Array, {@code null} will be returned. * - * @return the value as an Array or an empty Array. + * @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, an empty List will be returned. + * If the value isn't an Array, {@code null} will be returned. * - * @return the value as a List or an empty List. + * @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. + * + * @return the value as instance of Map. + * @throws JWTDecodeException if the value can't be converted to a Map. + */ + Map asMap() throws JWTDecodeException; + + /** + * 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 clazz) throws JWTDecodeException; } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/DecodedJWT.java b/lib/src/main/java/com/auth0/jwt/interfaces/DecodedJWT.java index b672b474..04307b28 100644 --- a/lib/src/main/java/com/auth0/jwt/interfaces/DecodedJWT.java +++ b/lib/src/main/java/com/auth0/jwt/interfaces/DecodedJWT.java @@ -3,6 +3,35 @@ /** * Class that represents a Json Web Token that was decoded from it's string representation. */ -public interface DecodedJWT extends Payload, Header, Signature { +public interface DecodedJWT extends Payload, Header { + /** + * Getter for the String Token used to create this JWT instance. + * + * @return the String Token. + */ String getToken(); + + /** + * Getter for the Header contained in the JWT as a Base64 encoded String. + * This represents the first part of the token. + * + * @return the Header of the JWT. + */ + String getHeader(); + + /** + * Getter for the Payload contained in the JWT as a Base64 encoded String. + * This represents the second part of the token. + * + * @return the Payload of the JWT. + */ + String getPayload(); + + /** + * Getter for the Signature contained in the JWT as a Base64 encoded String. + * This represents the third part of the token. + * + * @return the Signature of the JWT. + */ + String getSignature(); } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/ECDSAKeyProvider.java b/lib/src/main/java/com/auth0/jwt/interfaces/ECDSAKeyProvider.java new file mode 100644 index 00000000..55df451d --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/ECDSAKeyProvider.java @@ -0,0 +1,10 @@ +package com.auth0.jwt.interfaces; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +/** + * Elliptic Curve (EC) Public/Private Key provider. + */ +public interface ECDSAKeyProvider extends KeyProvider { +} 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 2af99788..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 { @@ -26,7 +26,6 @@ public interface Header { */ String getContentType(); - /** * Get the value of the "kid" claim, or null if it's not available. * @@ -35,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 new file mode 100644 index 00000000..30a144a6 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/KeyProvider.java @@ -0,0 +1,38 @@ +package com.auth0.jwt.interfaces; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * 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 + */ +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. + * @return the Public Key instance + */ + U getPublicKeyById(String keyId); + + /** + * Getter for the Private Key instance. Used to sign the content on the JWT signing stage. + * + * @return the Private Key instance + */ + 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. + * + * @return the Key Id that identifies the Private Key or null if it's not specified. + */ + String getPrivateKeyId(); +} 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 2fd93cc2..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,10 +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 { @@ -36,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. * @@ -43,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. * @@ -50,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. * @@ -58,10 +87,18 @@ public interface Payload { String getId(); /** - * Get a Private 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. */ Claim getClaim(String name); + + /** + * Get the Claims defined in the Token. + * + * @return a non-null Map containing the Claims defined in the Token. + */ + Map getClaims(); } diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/RSAKeyProvider.java b/lib/src/main/java/com/auth0/jwt/interfaces/RSAKeyProvider.java new file mode 100644 index 00000000..55376f4d --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/RSAKeyProvider.java @@ -0,0 +1,10 @@ +package com.auth0.jwt.interfaces; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +/** + * RSA Public/Private Key provider. + */ +public interface RSAKeyProvider extends KeyProvider { +} diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Signature.java b/lib/src/main/java/com/auth0/jwt/interfaces/Signature.java deleted file mode 100644 index ae190bf3..00000000 --- a/lib/src/main/java/com/auth0/jwt/interfaces/Signature.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.auth0.jwt.interfaces; - -/** - * The Signature class represents the 3rd part of the JWT, where the Signature value is hold. - */ -public interface Signature { - - /** - * Getter for the Signature contained in the JWT as a Base64 encoded String. - * - * @return the Signature of the JWT. - */ - String getSignature(); -} diff --git a/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java new file mode 100644 index 00000000..b4adcf5c --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/interfaces/Verification.java @@ -0,0 +1,267 @@ +package com.auth0.jwt.interfaces; + +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 { + + /** + * 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/ClockTest.java b/lib/src/test/java/com/auth0/jwt/ClockTest.java deleted file mode 100644 index 23cf8df3..00000000 --- a/lib/src/test/java/com/auth0/jwt/ClockTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.auth0.jwt; - -import org.junit.Test; - -import java.util.Date; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.*; - -public class ClockTest { - - @Test - public void shouldGetToday() throws Exception{ - Clock clock = new Clock(); - 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 92968b63..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,17 +139,7 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { concurrentVerify(verifier, token); } - - @Test - public void shouldPassECDSA256VerificationWithDERSignature() throws Exception { - String token = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; - ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); - Algorithm algorithm = Algorithm.ECDSA256(key); - JWTVerifier verifier = JWTVerifier.init(algorithm).withIssuer("auth0").build(); - - concurrentVerify(verifier, token); - } - + @Test public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { String token = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; @@ -160,16 +150,6 @@ public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { concurrentVerify(verifier, token); } - @Test - public void shouldPassECDSA384VerificationWithDERSignature() throws Exception { - String token = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; - ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); - Algorithm algorithm = Algorithm.ECDSA384(key); - JWTVerifier verifier = JWTVerifier.init(algorithm).withIssuer("auth0").build(); - - concurrentVerify(verifier, token); - } - @Test public void shouldPassECDSA512VerificationWithJOSESignature() throws Exception { String token = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AeCJPDIsSHhwRSGZCY6rspi8zekOw0K9qYMNridP1Fu9uhrA1QrG-EUxXlE06yvmh2R7Rz0aE7kxBwrnq8L8aOBCAYAsqhzPeUvyp8fXjjgs0Eto5I0mndE2QHlgcMSFASyjHbU8wD2Rq7ZNzGQ5b2MZfpv030WGUajT-aZYWFUJHVg2"; @@ -179,14 +159,4 @@ public void shouldPassECDSA512VerificationWithJOSESignature() throws Exception { concurrentVerify(verifier, token); } - - @Test - public void shouldPassECDSA512VerificationWithDERSignature() throws Exception { - String token = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; - ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); - Algorithm algorithm = Algorithm.ECDSA512(key); - JWTVerifier verifier = JWTVerifier.init(algorithm).withIssuer("auth0").build(); - - concurrentVerify(verifier, token); - } } diff --git a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java index fbb9d9a0..53cd267b 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTCreatorTest.java @@ -1,46 +1,261 @@ package com.auth0.jwt; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.RSAKeyProvider; +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; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.time.Instant; +import java.util.*; -import static org.hamcrest.Matchers.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; 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 shouldAddHeader() 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())); - assertThat(TokenUtils.splitToken(signed)[0], is("eyJhbGciOiJIUzI1NiIsImFzZCI6MTIzfQ")); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz")); } + @Test - public void shouldAddIssuer() 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.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "56a8bd44da435300010000015f5ed")); + } + + @Test + public void shouldAddKeyIdIfAvailableFromRSAAlgorithms() throws Exception { + RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA"); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .sign(Algorithm.RSA256(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + + @Test + public void shouldNotOverwriteKeyIdIfAddedFromRSAAlgorithms() throws Exception { + RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA"); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .withKeyId("real-key-id") + .sign(Algorithm.RSA256(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + + @Test + public void shouldAddKeyIdIfAvailableFromECDSAAlgorithms() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC"); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .sign(Algorithm.ECDSA256(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + + @Test + public void shouldNotOverwriteKeyIdIfAddedFromECDSAAlgorithms() throws Exception { + ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC"); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("my-key-id"); + when(provider.getPrivateKey()).thenReturn(privateKey); + + String signed = JWTCreator.init() + .withKeyId("real-key-id") + .sign(Algorithm.ECDSA256(provider)); + + assertThat(signed, is(notNullValue())); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id")); + } + + @Test + public void shouldAddIssuer() { String signed = JWTCreator.init() .withIssuer("auth0") .sign(Algorithm.HMAC256("secret")); @@ -50,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")); @@ -60,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")); @@ -78,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")); @@ -88,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")); @@ -98,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")); @@ -108,7 +344,17 @@ public void shouldAddIssuedAt() throws Exception { } @Test - public void shouldAddJWTId() throws Exception { + public void shouldAddIssuedAtInstant() { + String signed = JWTCreator.init() + .withIssuedAt(Instant.ofEpochSecond(1477592)) + .sign(Algorithm.HMAC256("secret")); + + assertThat(signed, is(notNullValue())); + assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9")); + } + + @Test + public void shouldAddJWTId() { String signed = JWTCreator.init() .withJWTId("jwt_id_123") .sign(Algorithm.HMAC256("secret")); @@ -118,27 +364,42 @@ public void shouldAddJWTId() throws Exception { } @Test - public void shouldRemoveClaimWhenPassingNull() 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.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS256")); + } + + @Test + public void shouldSetDefaultTypeInTheHeader() { String signed = JWTCreator.init() - .withIssuer("iss") - .withIssuer(null) .sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); - assertThat(TokenUtils.splitToken(signed)[1], is("e30")); + String[] parts = signed.split("\\."); + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8); + assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT")); } @Test - public void shouldSetCorrectAlgorithmInTheHeader() 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())); - assertThat(TokenUtils.splitToken(signed)[0], is("eyJhbGciOiJIUzI1NiJ9")); + 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() throws Exception { + public void shouldSetEmptySignatureIfAlgorithmIsNone() { String signed = JWTCreator.init() .sign(Algorithm.none()); assertThat(signed, is(notNullValue())); @@ -146,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() @@ -154,67 +415,646 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { } @Test - public void shouldThrowOnIllegalCustomClaimValueClass() throws Exception { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String."); - JWTCreator.init() - .withClaim("name", new Object()); + public void shouldAcceptCustomClaimOfTypeString() { + String jwt = JWTCreator.init() + .withClaim("name", "value") + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjoidmFsdWUifQ")); } @Test - public void shouldAcceptCustomClaimOfTypeString() throws Exception { + public void shouldAcceptCustomClaimOfTypeInteger() { String jwt = JWTCreator.init() - .withClaim("name", "value") + .withClaim("name", 123) .sign(Algorithm.HMAC256("secret")); - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidmFsdWUifQ.4qDWJcNQHDVDW1iAcIgZNiu-qqJQ0RIq8X3ETijBx5k"; assertThat(jwt, is(notNullValue())); - assertThat(jwt, is(token)); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjoxMjN9")); } @Test - public void shouldAcceptCustomClaimOfTypeInteger() throws Exception { + public void shouldAcceptCustomClaimOfTypeLong() { String jwt = JWTCreator.init() - .withClaim("name", 123) + .withClaim("name", Long.MAX_VALUE) .sign(Algorithm.HMAC256("secret")); - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoxMjN9.5i6ga8YMteicIeZrFZgJyW4OnI_2jpMaUXcDt-_jme4"; assertThat(jwt, is(notNullValue())); - assertThat(jwt, is(token)); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjo5MjIzMzcyMDM2ODU0Nzc1ODA3fQ")); } @Test - public void shouldAcceptCustomClaimOfTypeDouble() throws Exception { + public void shouldAcceptCustomClaimOfTypeDouble() { String jwt = JWTCreator.init() .withClaim("name", 23.45) .sign(Algorithm.HMAC256("secret")); - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoyMy40NX0.aFNlMk3WiikukJq1jo4Tf8ztR180wjTfSpqec0xKKqU"; assertThat(jwt, is(notNullValue())); - assertThat(jwt, is(token)); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjoyMy40NX0")); } @Test - public void shouldAcceptCustomClaimOfTypeBoolean() throws Exception { + public void shouldAcceptCustomClaimOfTypeBoolean() { String jwt = JWTCreator.init() .withClaim("name", true) .sign(Algorithm.HMAC256("secret")); - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp0cnVlfQ.jseAYuhVmT1boYrHQfn9wXmomWq_tdGfphLtG_2tj_M"; assertThat(jwt, is(notNullValue())); - assertThat(jwt, is(token)); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjp0cnVlfQ")); } @Test - public void shouldAcceptCustomClaimOfTypeDate() throws Exception { + public void shouldAcceptCustomClaimOfTypeDate() { Date date = new Date(1478891521000L); String jwt = JWTCreator.init() .withClaim("name", date) .sign(Algorithm.HMAC256("secret")); - String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.ZU1B1pDLYoJZhWD8h3_QsK5dViolxvL5Q43Yz9QIxL4"; assertThat(jwt, is(notNullValue())); - assertThat(jwt, is(token)); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjoxNDc4ODkxNTIxfQ")); + } + + @Test + 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")); } -} \ No newline at end of file + @Test + public void shouldAcceptCustomArrayClaimOfTypeString() { + String jwt = JWTCreator.init() + .withArrayClaim("name", new String[]{"text", "123", "true"}) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjpbInRleHQiLCIxMjMiLCJ0cnVlIl19")); + } + + @Test + public void shouldAcceptCustomArrayClaimOfTypeInteger() { + String jwt = JWTCreator.init() + .withArrayClaim("name", new Integer[]{1, 2, 3}) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); + } + + @Test + public void shouldAcceptCustomArrayClaimOfTypeLong() { + String jwt = JWTCreator.init() + .withArrayClaim("name", new Long[]{1L, 2L, 3L}) + .sign(Algorithm.HMAC256("secret")); + + assertThat(jwt, is(notNullValue())); + String[] parts = jwt.split("\\."); + assertThat(parts[1], is("eyJuYW1lIjpbMSwyLDNdfQ")); + } + + @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 b3e74f6e..cc427d60 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTDecoderTest.java @@ -1,18 +1,21 @@ 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.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; import static org.hamcrest.Matchers.*; @@ -22,188 +25,392 @@ public class JWTDecoderTest { public ExpectedException exception = ExpectedException.none(); @Test - public void getSubject() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); + public void getSubject() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"); assertThat(jwt.getSubject(), is(notNullValue())); assertThat(jwt.getSubject(), is("1234567890")); } // 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."); - JWTDecoder.decode("two.parts"); + 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."); - JWTDecoder.decode("this.has.four.parts"); + 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 = "{}}{"; + String invalidJson = "}{"; exception.expect(JWTDecodeException.class); exception.expectMessage(String.format("The string '%s' doesn't have a valid JSON format.", invalidJson)); customJWT(validJson, invalidJson, "signature"); } @Test - public void shouldThrowIfHeaderHasInvalidJSONFormat() throws Exception { + public void shouldThrowIfHeaderHasInvalidJSONFormat() { String validJson = "{}"; - String invalidJson = "{}}{"; + String invalidJson = "}{"; exception.expect(JWTDecodeException.class); exception.expectMessage(String.format("The string '%s' doesn't have a valid JSON format.", invalidJson)); customJWT(invalidJson, validJson, "signature"); } - // getToken @Test - public void shouldGetStringToken() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + 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() { + 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")); } - // Parts + @Test + public void shouldGetHeader() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getHeader(), is("eyJhbGciOiJIUzI1NiJ9")); + } @Test - public void shouldGetAlgorithm() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + public void shouldGetPayload() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); assertThat(jwt, is(notNullValue())); - assertThat(jwt.getAlgorithm(), is("HS256")); + assertThat(jwt.getPayload(), is("e30")); } @Test - public void shouldGetSignature() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NzY3MjcwODZ9.L9dcPHEDQew2u9MkDCORFkfDGcSOsgoPqNY-LUMLEHg"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0NzY3MjcwODZ9.tkpD3iCPQPVqjnjpDVp2bJMBAgpVCG9ZjlBuMitass0"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzY3MjcwODZ9.KPjGoW665E8V5_27Jugab8qSTxLk2cgquhPCBfAP0_w"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjM0NTY3ODkwIn0.m3zgEfVUFOd-CvL3xG5BuOWLzb0zMQZCqiVNQQOPOvA"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiIsImN0eSI6ImF3ZXNvbWUifQ.e30.AIm-pJDOaAyct9qKMlN-lQieqNDqc3d4erqUZc5SHAs"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.e30.WdFmrzx8b9v_a-r6EHC2PTAaWywgm_8LiP8RBRhYwkI"); + public void shouldGetType() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.e30.WdFmrzx8b9v_a-r6EHC2PTAaWywgm_8LiP8RBRhYwkI"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getType(), is("JWS")); } - //Private PublicClaims + @Test + public void shouldGetAlgorithm() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ"); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getAlgorithm(), is("HS256")); + } + + // Private Claims @Test - public void shouldGetMissingClaimIfClaimDoesNotExist() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.e30.K17vlwhE8FCMShdl1_65jEYqsQqBOVMPUU9IgG-QlTM"); + 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 { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifX0.lrU1gZlOdlmTTeZwq0VI-pZx2iV46UWYd5-lCjy6-c4"); + public void shouldGetValidClaim() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnsibmFtZSI6ImpvaG4ifX0.lrU1gZlOdlmTTeZwq0VI-pZx2iV46UWYd5-lCjy6-c4"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaim("object"), is(notNullValue())); assertThat(jwt.getClaim("object"), is(instanceOf(Claim.class))); } @Test - public void shouldGetNullClaimIfClaimValueIsNull() throws Exception { - DecodedJWT jwt = JWTDecoder.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnt9fQ.d3nUeeL_69QsrHL0ZWij612LHEQxD8EZg1rNoY3a4aI"); + public void shouldNotGetNullClaimIfClaimIsEmptyObject() { + DecodedJWT jwt = JWT.decode("eyJhbGciOiJIUzI1NiJ9.eyJvYmplY3QiOnt9fQ.d3nUeeL_69QsrHL0ZWij612LHEQxD8EZg1rNoY3a4aI"); assertThat(jwt, is(notNullValue())); assertThat(jwt.getClaim("object"), is(notNullValue())); - assertThat(jwt.getClaim("object").isNull(), is(true)); + assertThat(jwt.getClaim("object").isNull(), is(false)); + } + + @Test + public void shouldGetCustomClaimOfTypeInteger() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMjN9.XZAudnA7h3_Al5kJydzLjw6RzZC3Q6OvnLEYlhNW7HA"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asInt(), is(123)); + } + + @Test + public void shouldGetCustomClaimOfTypeDouble() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoyMy40NX0.7pyX2OmEGaU9q15T8bGFqRm-d3RVTYnqmZNZtxMKSlA"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asDouble(), is(23.45)); + } + + @Test + public void shouldGetCustomClaimOfTypeBoolean() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjp0cnVlfQ.FwQ8VfsZNRqBa9PXMinSIQplfLU4-rkCLfIlTLg_MV0"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asBoolean(), is(true)); + } + + @Test + public void shouldGetCustomClaimOfTypeDate() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; + Date date = new Date(1478891521000L); + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asDate().getTime(), is(date.getTime())); + } + + @Test + public void shouldGetCustomClaimOfTypeInstant() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; + Instant instant = Instant.ofEpochSecond(1478891521L); + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asInstant(), is(equalTo(instant))); + } + + @Test + public void shouldGetCustomArrayClaimOfTypeString() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbInRleHQiLCIxMjMiLCJ0cnVlIl19.lxM8EcmK1uSZRAPd0HUhXGZJdauRmZmLjoeqz4J9yAA"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asArray(String.class), arrayContaining("text", "123", "true")); + } + + @Test + public void shouldGetCustomArrayClaimOfTypeInteger() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + assertThat(jwt.getClaim("name").asArray(Integer.class), arrayContaining(1, 2, 3)); + } + + @Test + public void shouldGetCustomMapClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjp7InN0cmluZyI6InZhbHVlIiwibnVtYmVyIjoxLCJib29sZWFuIjp0cnVlLCJlbXB0eSI6bnVsbH19.6xkCuYZnu4RA0xZSxlYSYAqzy9JDWsDtIWqSCUZlPt8"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt, is(notNullValue())); + Map map = jwt.getClaim("name").asMap(); + assertThat(map, hasEntry("string", "value")); + assertThat(map, hasEntry("number", 1)); + assertThat(map, hasEntry("boolean", true)); + assertThat(map, hasEntry("empty", null)); + } + + @Test + public void shouldGetCustomNullClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpudWxsfQ.X4ALHe7uYqEcXWFBnwBUNRKwmwrtDEGZ2aynRYYUx8c"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt.getClaim("name").isNull(), is(true)); + } + + @Test + public void shouldGetListClaim() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbbnVsbCwiaGVsbG8iXX0.SpcuQRBGdTV0ofHdxBSnhWEUsQi89noZUXin2Thwb70"; + DecodedJWT jwt = JWT.decode(token); + assertThat(jwt.getClaim("name").asList(String.class), contains(null, "hello")); + } + + @Test + 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))); + assertThat(jwt.getClaims().get("exp"), is(notNullValue())); + assertThat(jwt.getClaims().get("iat"), is(notNullValue())); + assertThat(jwt.getClaims().get("nbf"), is(notNullValue())); + assertThat(jwt.getClaims().get("jti"), is(notNullValue())); + assertThat(jwt.getClaims().get("aud"), is(notNullValue())); + assertThat(jwt.getClaims().get("sub"), is(notNullValue())); + assertThat(jwt.getClaims().get("iss"), is(notNullValue())); + 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)); - return JWTDecoder.decode(String.format("%s.%s.%s", header, body, signature)); + 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 18e9cd9c..087f1e9e 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTTest.java @@ -3,19 +3,23 @@ import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; 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; +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 { @@ -29,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() @@ -66,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() @@ -76,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() @@ -86,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() @@ -165,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() @@ -179,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() @@ -190,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() @@ -201,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() @@ -212,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() @@ -220,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() @@ -232,64 +265,63 @@ 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"; - DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) - .build(clock) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .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"; - DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification .build(clock) .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"; - DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification .build(clock) .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"; - DecodedJWT jwt = JWT.require(Algorithm.HMAC256("secret")) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWT.require(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification .build() .verify(token); @@ -298,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() @@ -309,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() @@ -320,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() @@ -331,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() @@ -347,12 +379,15 @@ public void shouldGetCustomClaims() throws Exception { // Sign @Test - public void shouldCreateAnEmptyHMAC256SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJIUzI1NiJ9.e30."; - + public void shouldCreateAnEmptyHMAC256SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC256("secret")); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.HMAC256("secret")) .build(); @@ -360,12 +395,15 @@ public void shouldCreateAnEmptyHMAC256SignedToken() throws Exception { } @Test - public void shouldCreateAnEmptyHMAC384SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJIUzM4NCJ9.e30."; - + public void shouldCreateAnEmptyHMAC384SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC384("secret")); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.HMAC384("secret")) .build(); @@ -373,12 +411,15 @@ public void shouldCreateAnEmptyHMAC384SignedToken() throws Exception { } @Test - public void shouldCreateAnEmptyHMAC512SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJIUzUxMiJ9.e30."; - + public void shouldCreateAnEmptyHMAC512SignedToken() { String signed = JWT.create().sign(Algorithm.HMAC512("secret")); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.HMAC512("secret")) .build(); @@ -387,11 +428,14 @@ public void shouldCreateAnEmptyHMAC512SignedToken() throws Exception { @Test public void shouldCreateAnEmptyRSA256SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJSUzI1NiJ9.e30."; - String signed = JWT.create().sign(Algorithm.RSA256((RSAKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.RSA256((RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"))) .build(); @@ -400,11 +444,14 @@ public void shouldCreateAnEmptyRSA256SignedToken() throws Exception { @Test public void shouldCreateAnEmptyRSA384SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJSUzM4NCJ9.e30."; - String signed = JWT.create().sign(Algorithm.RSA384((RSAKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.RSA384((RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"))) .build(); @@ -413,11 +460,14 @@ public void shouldCreateAnEmptyRSA384SignedToken() throws Exception { @Test public void shouldCreateAnEmptyRSA512SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJSUzUxMiJ9.e30."; - String signed = JWT.create().sign(Algorithm.RSA512((RSAKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.RSA512((RSAKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_RSA, "RSA"))) .build(); @@ -426,11 +476,14 @@ public void shouldCreateAnEmptyRSA512SignedToken() throws Exception { @Test public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJFUzI1NiJ9.e30."; - String signed = JWT.create().sign(Algorithm.ECDSA256((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.ECDSA256((ECKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_256, "EC"))) .build(); @@ -439,11 +492,14 @@ public void shouldCreateAnEmptyECDSA256SignedToken() throws Exception { @Test public void shouldCreateAnEmptyECDSA384SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJFUzM4NCJ9.e30."; - String signed = JWT.create().sign(Algorithm.ECDSA384((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_384, "EC"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.ECDSA384((ECKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_384, "EC"))) .build(); @@ -452,11 +508,14 @@ public void shouldCreateAnEmptyECDSA384SignedToken() throws Exception { @Test public void shouldCreateAnEmptyECDSA512SignedToken() throws Exception { - String headerAndPayload = "eyJhbGciOiJFUzUxMiJ9.e30."; - String signed = JWT.create().sign(Algorithm.ECDSA512((ECKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_512, "EC"))); assertThat(signed, is(notNullValue())); - assertThat(signed, startsWith(headerAndPayload)); + + String[] parts = signed.split("\\."); + 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")); JWTVerifier verified = JWT.require(Algorithm.ECDSA512((ECKey) PemUtils.readPublicKeyFromFile(PUBLIC_KEY_FILE_EC_512, "EC"))) .build(); diff --git a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java index fe7b1cf5..732d6365 100644 --- a/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java +++ b/lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java @@ -1,67 +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.*; +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") @@ -72,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") @@ -92,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") @@ -102,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") @@ -114,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 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 shouldThrowOnNullCustomClaimName() throws Exception { + 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")) @@ -133,85 +346,199 @@ public void shouldThrowOnNullCustomClaimName() throws Exception { } @Test - public void shouldThrowOnIllegalCustomClaimValueClass() throws Exception { - exception.expect(IllegalArgumentException.class); - exception.expectMessage("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String."); - JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", new Object()); + 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 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")) + public void shouldThrowWhenExpectedClaimIsMissing() { + MissingClaimException e = assertThrows(null, MissingClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbSI6InRleHQifQ.aZ27Ze35VvTqxpaSIK5ZcnYHr4SrvANlUbDR8fw9qsQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("missing", "text") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'missing' is not present in the JWT.")); + assertThat(e.getClaimName(), is("missing")); + } + + @Test + public void shouldThrowOnInvalidCustomClaimValueOfTypeString() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", "value") + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); + } + + @Test + public void shouldThrowOnInvalidCustomClaimValueOfTypeInteger() { + 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 shouldThrowOnInvalidCustomClaimValueOfTypeBoolean() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", true) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); + } + + + @Test + public void shouldThrowOnInvalidCustomClaimValueOfTypeDate() { + IncorrectClaimException e = assertThrows(null, IncorrectClaimException.class, () -> { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjpbInNvbWV0aGluZyJdfQ.3ENLez6tU_fG0SVFrGmISltZPiXLSHaz_dyn-XFTEGQ"; + JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", new Date()) + .build() + .verify(token); + }); + assertThat(e.getMessage(), is("The Claim 'name' value doesn't match the required one.")); + assertThat(e.getClaimName(), is("name")); + assertThat(e.getClaimValue().asArray(String.class), is(new String[] {"something"})); + } + + @Test + public void shouldThrowOnInvalidCustomClaimValue() { + 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") .build() .verify(token); + + assertThat(jwt, is(notNullValue())); } @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")) + public void shouldValidateCustomClaimOfTypeInteger() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMjN9.XZAudnA7h3_Al5kJydzLjw6RzZC3Q6OvnLEYlhNW7HA"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", 123) .build() .verify(token); + + assertThat(jwt, is(notNullValue())); } @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")) + public void shouldValidateCustomClaimOfTypeLong() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjo5MjIzMzcyMDM2ODU0Nzc2MDB9.km-IwQ5IDnTZFmuJzhSgvjTzGkn_Z5X29g4nAuVC56I"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", 922337203685477600L) + .build() + .verify(token); + + assertThat(jwt, is(notNullValue())); + } + + @Test + public void shouldValidateCustomClaimOfTypeDouble() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoyMy40NX0.7pyX2OmEGaU9q15T8bGFqRm-d3RVTYnqmZNZtxMKSlA"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", 23.45) .build() .verify(token); + + assertThat(jwt, is(notNullValue())); } @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")) + public void shouldValidateCustomClaimOfTypeBoolean() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjp0cnVlfQ.FwQ8VfsZNRqBa9PXMinSIQplfLU4-rkCLfIlTLg_MV0"; + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) .withClaim("name", true) .build() .verify(token); - } + assertThat(jwt, is(notNullValue())); + } @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()) + public void shouldValidateCustomClaimOfTypeDate() { + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; + Date date = new Date(1478891521123L); + DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) + .withClaim("name", date) .build() .verify(token); + + assertThat(jwt, is(notNullValue())); } @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 Clock()); - verifier.verify(token); + 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 shouldValidateCustomClaimOfTypeString() throws Exception { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidmFsdWUifQ.Jki8pvw6KGbxpMinufrgo6RDL1cu7AtNMJYVh6t-_cE"; + public void shouldValidateCustomArrayClaimOfTypeString() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbInRleHQiLCIxMjMiLCJ0cnVlIl19.lxM8EcmK1uSZRAPd0HUhXGZJdauRmZmLjoeqz4J9yAA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", "value") + .withArrayClaim("name", "text", "123", "true") .build() .verify(token); @@ -219,10 +546,10 @@ public void shouldValidateCustomClaimOfTypeString() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeInteger() throws Exception { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxMjN9.XZAudnA7h3_Al5kJydzLjw6RzZC3Q6OvnLEYlhNW7HA"; + public void shouldValidateCustomArrayClaimOfTypeInteger() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 123) + .withArrayClaim("name", 1, 2, 3) .build() .verify(token); @@ -230,10 +557,10 @@ public void shouldValidateCustomClaimOfTypeInteger() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeDouble() throws Exception { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoyMy40NX0.7pyX2OmEGaU9q15T8bGFqRm-d3RVTYnqmZNZtxMKSlA"; + public void shouldValidateCustomArrayClaimOfTypeLong() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbNTAwMDAwMDAwMDAxLDUwMDAwMDAwMDAwMiw1MDAwMDAwMDAwMDNdfQ.vzV7S0gbV9ZAVxChuIt4XZuSVTxMH536rFmoHzxmayM"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", 23.45) + .withArrayClaim("name", 500000000001L, 500000000002L, 500000000003L) .build() .verify(token); @@ -241,10 +568,10 @@ public void shouldValidateCustomClaimOfTypeDouble() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeBoolean() throws Exception { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjp0cnVlfQ.FwQ8VfsZNRqBa9PXMinSIQplfLU4-rkCLfIlTLg_MV0"; + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsInteger() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSwyLDNdfQ.UEuMKRQYrzKAiPpPLhIVawWkKWA1zj0_GderrWUIyFE"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", true) + .withArrayClaim("name", 1L, 2L, 3L) .build() .verify(token); @@ -252,93 +579,91 @@ public void shouldValidateCustomClaimOfTypeBoolean() throws Exception { } @Test - public void shouldValidateCustomClaimOfTypeDate() throws Exception { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoxNDc4ODkxNTIxfQ.mhioumeok8fghQEhTKF3QtQAksSvZ_9wIhJmgZLhJ6c"; - Date date = new Date(1478891521000L); + public void shouldValidateCustomArrayClaimOfTypeLongWhenValueIsIntegerAndLong() { + String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpbMSw1MDAwMDAwMDAwMDIsNTAwMDAwMDAwMDAzXX0.PQjb2rPPpYjM2sItZEzZcjS2YbfPCp6xksTSPjpjTQA"; DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .withClaim("name", date) + .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); @@ -347,48 +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"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .acceptExpiresAt(2) - .build(clock) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) + .acceptExpiresAt(2); + DecodedJWT jwt = verification + .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"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .build(clock) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .build(mockOneSecondEarlier) .verify(token); assertThat(jwt, is(notNullValue())); } @Test - public void shouldThrowOnInvalidExpiresAtIfPresent() throws Exception { - exception.expect(InvalidClaimException.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.init(Algorithm.HMAC256("secret")) - .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); @@ -398,47 +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"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .acceptNotBefore(2) - .build(clock) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")) + .acceptNotBefore(2); + DecodedJWT jwt = verification + .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"; - JWTVerifier.init(Algorithm.HMAC256("secret")) - .build(clock) + @Test + public void shouldValidateNotBeforeIfPresent() { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTN9.f4zVV0TbbTG5xxDjSoGZ320JIMchGoQCWrnT5MyQdT0"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .build(mockOneSecondLater) .verify(token); + + assertThat(jwt, is(notNullValue())); } @Test - public void shouldValidateNotBeforeIfPresent() throws Exception { - Clock clock = mock(Clock.class); - when(clock.getToday()).thenReturn(new Date(DATE_TOKEN_MS_VALUE)); - - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0Nzc1OTJ9.isvT0Pqx0yjnZk53mUFSeYFJLDs-Ls9IsNAm86gIdZo"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .build(clock) + public void shouldAcceptNotBeforeEqualToNow() { + String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE0Nzc1OTJ9.71XBtRmkAa4iKnyhbS4NPW-Xr26eAVAdHZgmupS7a5o"; + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .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); @@ -446,49 +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"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .acceptIssuedAt(2) - .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.init(Algorithm.HMAC256("secret")) - .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"; - DecodedJWT jwt = JWTVerifier.init(Algorithm.HMAC256("secret")) - .build(clock) + JWTVerifier.BaseVerification verification = (JWTVerifier.BaseVerification) JWTVerifier.init(Algorithm.HMAC256("secret")); + DecodedJWT jwt = verification + .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); @@ -497,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") @@ -508,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() @@ -539,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 new file mode 100644 index 00000000..b09ab187 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/JsonMatcher.java @@ -0,0 +1,140 @@ +package com.auth0.jwt; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import java.lang.reflect.Array; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class JsonMatcher extends TypeSafeDiagnosingMatcher { + + private final String entry; + private final String key; + private final Matcher matcher; + + private JsonMatcher(String key, Object value, Matcher valueMatcher) { + this.key = key; + this.matcher = valueMatcher; + if (value != null) { + String stringValue = objectToString(value); + entry = getStringKey(key) + stringValue; + } else { + entry = null; + } + } + + @Override + protected boolean matchesSafely(String item, Description mismatchDescription) { + if (item == null) { + mismatchDescription.appendText("JSON was null"); + return false; + } + if (matcher != null) { + if (!matcher.matches(item)) { + matcher.describeMismatch(item, mismatchDescription); + return false; + } + if (!item.contains(getStringKey(key))) { + mismatchDescription.appendText("JSON didn't contained the key ").appendValue(key); + return false; + } + } + if (entry != null && !item.contains(entry)) { + mismatchDescription.appendText("JSON was ").appendValue(item); + return false; + } + + return true; + } + + @Override + public void describeTo(Description description) { + if (matcher == null) { + description.appendText("A JSON with entry ") + .appendValue(entry); + } else { + matcher.describeTo(description); + } + } + + public static JsonMatcher hasEntry(String key, Object value) { + return new JsonMatcher(key, value, null); + } + + 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 + "\":"; + } + + private String objectToString(Object value) { + String stringValue; + if (value == null) { + stringValue = "null"; + } else if (value instanceof String) { + stringValue = "\"" + value + "\""; + } else if (value instanceof Map) { + stringValue = mapToString((Map) value); + } else if (value instanceof Array) { + stringValue = arrayToString((Object[]) value); + } else if (value instanceof List) { + stringValue = listToString((List) value); + } else { + stringValue = value.toString(); + } + return stringValue; + } + + private String arrayToString(Object[] array) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < array.length; i++) { + Object o = array[i]; + sb.append(objectToString(o)); + if (i + 1 < array.length) { + sb.append(","); + } + } + sb.append("]"); + return sb.toString(); + } + + private String listToString(List list) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + Iterator it = list.iterator(); + while (it.hasNext()) { + Object o = it.next(); + sb.append(objectToString(o)); + if (it.hasNext()) { + sb.append(","); + } + } + sb.append("]"); + return sb.toString(); + } + + private String mapToString(Map map) { + StringBuilder sb = new StringBuilder(); + sb.append("{"); + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = it.next(); + sb.append("\"" + e.getKey() + "\":" + objectToString(e.getValue())); + if (it.hasNext()) { + sb.append(","); + } + } + sb.append("}"); + return sb.toString(); + } +} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/PemUtils.java b/lib/src/test/java/com/auth0/jwt/PemUtils.java index abfa65c6..6c92e05e 100644 --- a/lib/src/test/java/com/auth0/jwt/PemUtils.java +++ b/lib/src/test/java/com/auth0/jwt/PemUtils.java @@ -3,12 +3,14 @@ import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; -import java.io.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.interfaces.ECPublicKey; import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; @@ -22,7 +24,9 @@ private static byte[] parsePEMFile(File pemFile) throws IOException { } PemReader reader = new PemReader(new FileReader(pemFile)); PemObject pemObject = reader.readPemObject(); - return pemObject.getContent(); + byte[] content = pemObject.getContent(); + reader.close(); + return content; } private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) { @@ -39,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 73b23dbb..e09661d3 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmTest.java @@ -1,16 +1,22 @@ package com.auth0.jwt.algorithms; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.RSAKeyProvider; 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.ECKey; -import java.security.interfaces.RSAKey; +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 { @@ -19,7 +25,7 @@ public class AlgorithmTest { @Test - public void shouldThrowHMAC256VerificationWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC256InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -27,7 +33,7 @@ public void shouldThrowHMAC256VerificationWithNullSecretBytes() throws Exception } @Test - public void shouldThrowHMAC384VerificationWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC384InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -35,7 +41,7 @@ public void shouldThrowHMAC384VerificationWithNullSecretBytes() throws Exception } @Test - public void shouldThrowHMAC512VerificationWithNullSecretBytes() throws Exception { + public void shouldThrowHMAC512InstanceWithNullSecretBytes() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); byte[] secret = null; @@ -43,7 +49,7 @@ public void shouldThrowHMAC512VerificationWithNullSecretBytes() throws Exception } @Test - public void shouldThrowHMAC256VerificationWithNullSecret() throws Exception { + public void shouldThrowHMAC256InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -51,7 +57,7 @@ public void shouldThrowHMAC256VerificationWithNullSecret() throws Exception { } @Test - public void shouldThrowHMAC384VerificationWithNullSecret() throws Exception { + public void shouldThrowHMAC384InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -59,7 +65,7 @@ public void shouldThrowHMAC384VerificationWithNullSecret() throws Exception { } @Test - public void shouldThrowHMAC512VerificationWithNullSecret() throws Exception { + public void shouldThrowHMAC512InstanceWithNullSecret() { exception.expect(IllegalArgumentException.class); exception.expectMessage("The Secret cannot be null"); String secret = null; @@ -67,187 +73,475 @@ public void shouldThrowHMAC512VerificationWithNullSecret() throws Exception { } @Test - public void shouldThrowRSA256VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA256InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The RSAKey cannot be null"); - Algorithm.RSA256(null); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA256(key); } @Test - public void shouldThrowRSA384VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA256InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The RSAKey cannot be null"); - Algorithm.RSA384(null); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA256(null, null); } @Test - public void shouldThrowRSA512VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA256InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The RSAKey cannot be null"); - Algorithm.RSA512(null); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA256(provider); } @Test - public void shouldThrowECDSA256VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA384InstanceWithNullKey() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The ECKey cannot be null"); - Algorithm.ECDSA256(null); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA384(key); } @Test - public void shouldThrowECDSA384VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA384InstanceWithNullKeys() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The ECKey cannot be null"); - Algorithm.ECDSA384(null); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA384(null, null); } @Test - public void shouldThrowECDSA512VerificationWithNullPublicKey() throws Exception { + public void shouldThrowRSA384InstanceWithNullKeyProvider() { exception.expect(IllegalArgumentException.class); - exception.expectMessage("The ECKey cannot be null"); - Algorithm.ECDSA512(null); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA384(provider); } @Test - public void shouldCreateHMAC256AlgorithmWithBytes() throws Exception { + public void shouldThrowRSA512InstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + RSAKey key = null; + Algorithm.RSA512(key); + } + + @Test + public void shouldThrowRSA512InstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.RSA512(null, null); + } + + @Test + public void shouldThrowRSA512InstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + RSAKeyProvider provider = null; + Algorithm.RSA512(provider); + } + + @Test + public void shouldThrowECDSA256InstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + ECKey key = null; + Algorithm.ECDSA256(key); + } + + @Test + public void shouldThrowECDSA256InstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.ECDSA256(null, null); + } + + @Test + public void shouldThrowECDSA256InstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + ECDSAKeyProvider provider = null; + Algorithm.ECDSA256(provider); + } + + @Test + public void shouldThrowECDSA384InstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + ECKey key = null; + Algorithm.ECDSA384(key); + } + + @Test + public void shouldThrowECDSA384InstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.ECDSA384(null, null); + } + + @Test + public void shouldThrowECDSA384InstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + ECDSAKeyProvider provider = null; + Algorithm.ECDSA384(provider); + } + + @Test + public void shouldThrowECDSA512InstanceWithNullKey() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + ECKey key = null; + Algorithm.ECDSA512(key); + } + + @Test + public void shouldThrowECDSA512InstanceWithNullKeys() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Both provided Keys cannot be null."); + Algorithm.ECDSA512(null, null); + } + + @Test + public void shouldThrowECDSA512InstanceWithNullKeyProvider() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("The Key Provider cannot be null."); + ECDSAKeyProvider provider = null; + Algorithm.ECDSA512(provider); + } + + @Test + public void shouldCreateHMAC256AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA256")); assertThat(algorithm.getName(), is("HS256")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateHMAC384AlgorithmWithBytes() throws Exception { + public void shouldCreateHMAC384AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA384")); assertThat(algorithm.getName(), is("HS384")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateHMAC512AlgorithmWithBytes() throws Exception { + public void shouldCreateHMAC512AlgorithmWithBytes() { Algorithm algorithm = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA512")); assertThat(algorithm.getName(), is("HS512")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateHMAC256AlgorithmWithString() throws Exception { + public void shouldCreateHMAC256AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC256("secret"); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA256")); assertThat(algorithm.getName(), is("HS256")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateHMAC384AlgorithmWithString() throws Exception { + public void shouldCreateHMAC384AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC384("secret"); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA384")); assertThat(algorithm.getName(), is("HS384")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateHMAC512AlgorithmWithString() throws Exception { + public void shouldCreateHMAC512AlgorithmWithString() { Algorithm algorithm = Algorithm.HMAC512("secret"); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(HMACAlgorithm.class))); assertThat(algorithm.getDescription(), is("HmacSHA512")); assertThat(algorithm.getName(), is("HS512")); - assertThat(((HMACAlgorithm) algorithm).getSecret(), is("secret".getBytes(StandardCharsets.UTF_8))); } @Test - public void shouldCreateRSA256Algorithm() throws Exception { - RSAKey key = mock(RSAKey.class); + public void shouldCreateRSA256AlgorithmWithPublicKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA256(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA256withRSA")); assertThat(algorithm.getName(), is("RS256")); - assertThat(((RSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateRSA384Algorithm() throws Exception { - RSAKey key = mock(RSAKey.class); + public void shouldCreateRSA256AlgorithmWithPrivateKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); + Algorithm algorithm = Algorithm.RSA256(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withRSA")); + assertThat(algorithm.getName(), is("RS256")); + } + + @Test + public void shouldCreateRSA256AlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withRSA")); + assertThat(algorithm.getName(), is("RS256")); + } + + @Test + public void shouldCreateRSA256AlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA256(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withRSA")); + assertThat(algorithm.getName(), is("RS256")); + } + + @Test + public void shouldCreateRSA384AlgorithmWithPublicKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA384(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA384withRSA")); assertThat(algorithm.getName(), is("RS384")); - assertThat(((RSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateRSA512Algorithm() throws Exception { - RSAKey key = mock(RSAKey.class); + public void shouldCreateRSA384AlgorithmWithPrivateKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); + Algorithm algorithm = Algorithm.RSA384(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withRSA")); + assertThat(algorithm.getName(), is("RS384")); + } + + @Test + public void shouldCreateRSA384AlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA384(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withRSA")); + assertThat(algorithm.getName(), is("RS384")); + } + + @Test + public void shouldCreateRSA384AlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA384(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withRSA")); + assertThat(algorithm.getName(), is("RS384")); + } + + @Test + public void shouldCreateRSA512AlgorithmWithPublicKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); Algorithm algorithm = Algorithm.RSA512(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA512withRSA")); assertThat(algorithm.getName(), is("RS512")); - assertThat(((RSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateECDSA256Algorithm() throws Exception { - ECKey key = mock(ECKey.class); + public void shouldCreateRSA512AlgorithmWithPrivateKey() { + RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); + Algorithm algorithm = Algorithm.RSA512(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withRSA")); + assertThat(algorithm.getName(), is("RS512")); + } + + @Test + public void shouldCreateRSA512AlgorithmWithBothKeys() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + Algorithm algorithm = Algorithm.RSA512(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withRSA")); + assertThat(algorithm.getName(), is("RS512")); + } + + @Test + public void shouldCreateRSA512AlgorithmWithProvider() { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + Algorithm algorithm = Algorithm.RSA512(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(RSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withRSA")); + assertThat(algorithm.getName(), is("RS512")); + } + + @Test + public void shouldCreateECDSA256AlgorithmWithPublicKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); Algorithm algorithm = Algorithm.ECDSA256(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA256withECDSA")); assertThat(algorithm.getName(), is("ES256")); - assertThat(((ECDSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateECDSA384Algorithm() throws Exception { - ECKey key = mock(ECKey.class); + public void shouldCreateECDSA256AlgorithmWithPrivateKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); + Algorithm algorithm = Algorithm.ECDSA256(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256")); + } + + @Test + public void shouldCreateECDSA256AlgorithmWithBothKeys() { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + Algorithm algorithm = Algorithm.ECDSA256(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256")); + } + + @Test + public void shouldCreateECDSA256AlgorithmWithProvider() { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + Algorithm algorithm = Algorithm.ECDSA256(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA256withECDSA")); + assertThat(algorithm.getName(), is("ES256")); + } + + @Test + public void shouldCreateECDSA384AlgorithmWithPublicKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); + Algorithm algorithm = Algorithm.ECDSA384(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withECDSA")); + assertThat(algorithm.getName(), is("ES384")); + } + + @Test + public void shouldCreateECDSA384AlgorithmWithPrivateKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); Algorithm algorithm = Algorithm.ECDSA384(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA384withECDSA")); assertThat(algorithm.getName(), is("ES384")); - assertThat(((ECDSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateECDSA512Algorithm() throws Exception { - ECKey key = mock(ECKey.class); + public void shouldCreateECDSA384AlgorithmWithBothKeys() { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + Algorithm algorithm = Algorithm.ECDSA384(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withECDSA")); + assertThat(algorithm.getName(), is("ES384")); + } + + @Test + public void shouldCreateECDSA384AlgorithmWithProvider() { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + Algorithm algorithm = Algorithm.ECDSA384(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA384withECDSA")); + assertThat(algorithm.getName(), is("ES384")); + } + + @Test + public void shouldCreateECDSA512AlgorithmWithPublicKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); Algorithm algorithm = Algorithm.ECDSA512(key); assertThat(algorithm, is(notNullValue())); assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); assertThat(algorithm.getDescription(), is("SHA512withECDSA")); assertThat(algorithm.getName(), is("ES512")); - assertThat(((ECDSAAlgorithm) algorithm).getKey(), is(key)); } @Test - public void shouldCreateNoneAlgorithm() throws Exception { + public void shouldCreateECDSA512AlgorithmWithPrivateKey() { + ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); + Algorithm algorithm = Algorithm.ECDSA512(key); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withECDSA")); + assertThat(algorithm.getName(), is("ES512")); + } + + @Test + public void shouldCreateECDSA512AlgorithmWithBothKeys() { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + Algorithm algorithm = Algorithm.ECDSA512(publicKey, privateKey); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withECDSA")); + assertThat(algorithm.getName(), is("ES512")); + } + + @Test + public void shouldCreateECDSA512AlgorithmWithProvider() { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + Algorithm algorithm = Algorithm.ECDSA512(provider); + + assertThat(algorithm, is(notNullValue())); + assertThat(algorithm, is(instanceOf(ECDSAAlgorithm.class))); + assertThat(algorithm.getDescription(), is("SHA512withECDSA")); + assertThat(algorithm.getName(), is("ES512")); + } + + @Test + public void shouldCreateNoneAlgorithm() { Algorithm algorithm = Algorithm.none(); assertThat(algorithm, is(notNullValue())); @@ -256,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/AlgorithmUtils.java b/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmUtils.java deleted file mode 100644 index c8eaccfa..00000000 --- a/lib/src/test/java/com/auth0/jwt/algorithms/AlgorithmUtils.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.auth0.jwt.algorithms; - -import org.apache.commons.codec.binary.Base64; - -import java.nio.charset.StandardCharsets; - -public class AlgorithmUtils { - - public static void verify(Algorithm algorithm, String jwt) { - String[] parts = jwt.split("\\."); - byte[] content = String.format("%s.%s", parts[0], parts[1]).getBytes(StandardCharsets.UTF_8); - byte[] signature = new byte[0]; - if (parts.length == 3) { - signature = Base64.decodeBase64(parts[2]); - } - algorithm.verify(content, signature); - } -} 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 c2b8f574..2e636c71 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSAAlgorithmTest.java @@ -1,34 +1,47 @@ package com.auth0.jwt.algorithms; +import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; -import org.apache.commons.codec.binary.Base64; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.JWTVerifier; +import org.hamcrest.Matchers; +import org.hamcrest.collection.IsIn; 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 org.hamcrest.Matchers.*; +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.isA; -import static org.junit.Assert.assertThat; +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.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; 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"; @@ -43,6 +56,8 @@ public class ECDSAAlgorithmTest { //JOSE Signatures obtained using Node 'jwa' lib: https://github.com/brianloveswords/node-jwa //DER Signatures obtained from source JOSE signature using 'ecdsa-sig-formatter' lib: https://github.com/Brightspace/node-ecdsa-sig-formatter + //These tests use the default preferred SecurityProvider to handle ECDSA algorithms + // Verify @Test @@ -50,15 +65,62 @@ public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); Algorithm algorithm = Algorithm.ECDSA256(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldPassECDSA256VerificationWithDERSignature() throws Exception { - String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jS/hFPj/0hpCWn7x1n/h+xPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + public void shouldThrowOnECDSA256VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); Algorithm algorithm = Algorithm.ECDSA256(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA256VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA256VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "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)); + } + + @Test + public void shouldPassECDSA256VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.D_oU4CB0ZEsxHOjcWnmS3ZJvlTzm6WcGFx-HASxnvcB2Xu2WjI-axqXH9xKq45aPBDs330JpRhJmqBSc2K8MXQ"; + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.D_oU4CB0ZEsxHOjcWnmS3ZJvlTzm6WcGFx-HASxnvcB2Xu2WjI-axqXH9xKq45aPBDs330JpRhJmqBSc2K8MXQ"; + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -67,33 +129,33 @@ public void shouldFailECDSA256VerificationWithInvalidPublicKey() throws Exceptio exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.W9qfN1b80B9hnMo49WL8THrOsf1vEjOhapeFemPMGySzxTcgfyudS5esgeBTO908X5SLdAr5jMwPUPBs9b6nNg"; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailECDSA256VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not an ECPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.W9qfN1b80B9hnMo49WL8THrOsf1vEjOhapeFemPMGySzxTcgfyudS5esgeBTO908X5SLdAr5jMwPUPBs9b6nNg"; Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldFailECDSA256VerificationOnInvalidSignatureLength() throws Exception { + public void shouldFailECDSA256VerificationOnInvalidJOSESignatureLength() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("The signature length was invalid. Expected 64 bytes but received 63"))); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -103,10 +165,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -117,10 +179,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -128,15 +190,62 @@ public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); Algorithm algorithm = Algorithm.ECDSA384(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldPassECDSA384VerificationWithDERSignature() throws Exception { - String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXB/KRjyNAEqm+4dmh7ohkEmbk2+gHxtH6GdGDq2L4Idua+hG2Ut+ccCMH8CE2v/HCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAur+DEv8w=="; + public void shouldThrowOnECDSA384VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UtccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); Algorithm algorithm = Algorithm.ECDSA384(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA384VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; + Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA384VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + 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)); + } + + @Test + public void shouldPassECDSA384VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.9kjGuFTPx3ylfpqL0eY9H7TGmPepjQOBKI8UPoEvby6N7dDLF5HxLohosNxxFymNT7LzpeSgOPAB0wJEwG2Nl2ukgdUOpZOf492wog_i5ZcZmAykd3g1QH7onrzd69GU"; + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.9kjGuFTPx3ylfpqL0eY9H7TGmPepjQOBKI8UPoEvby6N7dDLF5HxLohosNxxFymNT7LzpeSgOPAB0wJEwG2Nl2ukgdUOpZOf492wog_i5ZcZmAykd3g1QH7onrzd69GU"; + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -145,33 +254,33 @@ public void shouldFailECDSA384VerificationWithInvalidPublicKey() throws Exceptio exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9._k5h1KyO-NE0R2_HAw0-XEc0bGT5atv29SxHhOGC9JDqUHeUdptfCK_ljQ01nLVt2OQWT2SwGs-TuyHDFmhPmPGFZ9wboxvq_ieopmYqhQilNAu-WF-frioiRz9733fU"; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailECDSA384VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not an ECPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9._k5h1KyO-NE0R2_HAw0-XEc0bGT5atv29SxHhOGC9JDqUHeUdptfCK_ljQ01nLVt2OQWT2SwGs-TuyHDFmhPmPGFZ9wboxvq_ieopmYqhQilNAu-WF-frioiRz9733fU"; Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldFailECDSA384VerificationOnInvalidSignatureLength() throws Exception { + public void shouldFailECDSA384VerificationOnInvalidJOSESignatureLength() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("The signature length was invalid. Expected 96 bytes but received 95"))); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -181,10 +290,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -195,10 +304,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -206,15 +315,62 @@ public void shouldPassECDSA512VerificationWithJOSESignature() throws Exception { String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AeCJPDIsSHhwRSGZCY6rspi8zekOw0K9qYMNridP1Fu9uhrA1QrG-EUxXlE06yvmh2R7Rz0aE7kxBwrnq8L8aOBCAYAsqhzPeUvyp8fXjjgs0Eto5I0mndE2QHlgcMSFASyjHbU8wD2Rq7ZNzGQ5b2MZfpv030WGUajT-aZYWFUJHVg2"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); Algorithm algorithm = Algorithm.ECDSA512(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldPassECDSA512VerificationWithDERSignature() throws Exception { - String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0/UW726GsDVCsb4RTFeUTTrK+aHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0/mmWFhVCR1YNg=="; + public void shouldThrowOnECDSA512VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); Algorithm algorithm = Algorithm.ECDSA512(key); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA512VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AeCJPDIsSHhwRSGZCY6rspi8zekOw0K9qYMNridP1Fu9uhrA1QrG-EUxXlE06yvmh2R7Rz0aE7kxBwrnq8L8aOBCAYAsqhzPeUvyp8fXjjgs0Eto5I0mndE2QHlgcMSFASyjHbU8wD2Rq7ZNzGQ5b2MZfpv030WGUajT-aZYWFUJHVg2"; + Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowECDSA512VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + 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)); + } + + @Test + public void shouldPassECDSA512VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.AGxEwbsYa2bQ7Y7DAcTQnVD8PmLSlhJ20jg2OfdyPnqdXI8SgBaG6lGciq3_pofFhs1HEoFoJ33Jcluha24oMHIvAfwu8qbv_Wq3L2eI9Q0L0p6ul8Pd_BS8adRa2PgLc36xXGcRc7ID5YH-CYaQfsTp5YIaF0Po3h0QyCoQ6ZiYQkqm"; + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.AGxEwbsYa2bQ7Y7DAcTQnVD8PmLSlhJ20jg2OfdyPnqdXI8SgBaG6lGciq3_pofFhs1HEoFoJ33Jcluha24oMHIvAfwu8qbv_Wq3L2eI9Q0L0p6ul8Pd_BS8adRa2PgLc36xXGcRc7ID5YH-CYaQfsTp5YIaF0Po3h0QyCoQ6ZiYQkqm"; + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -223,33 +379,33 @@ public void shouldFailECDSA512VerificationWithInvalidPublicKey() throws Exceptio exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AZgdopFFsN0amCSs2kOucXdpylD31DEm5ChK1PG0_gq5Mf47MrvVph8zHSVuvcrXzcE1U3VxeCg89mYW1H33Y-8iAF0QFkdfTUQIWKNObH543WNMYYssv3OtOj0znPv8atDbaF8DMYAtcT1qdmaSJRhx-egRE9HGZkinPh9CfLLLt58X"; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailECDSA512VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not an ECPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AZgdopFFsN0amCSs2kOucXdpylD31DEm5ChK1PG0_gq5Mf47MrvVph8zHSVuvcrXzcE1U3VxeCg89mYW1H33Y-8iAF0QFkdfTUQIWKNObH543WNMYYssv3OtOj0znPv8atDbaF8DMYAtcT1qdmaSJRhx-egRE9HGZkinPh9CfLLLt58X"; Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldFailECDSA512VerificationOnInvalidSignatureLength() throws Exception { + public void shouldFailECDSA512VerificationOnInvalidJOSESignatureLength() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("The signature length was invalid. Expected 132 bytes but received 131"))); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -259,10 +415,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -273,10 +429,10 @@ 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")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -284,15 +440,18 @@ public void shouldFailJOSEToDERConversionOnInvalidJOSESignatureLength() throws E exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); exception.expectCause(isA(SignatureException.class)); - exception.expectCause(hasMessage(is("Invalid ECDSA signature format"))); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); 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; - Algorithm algorithm = new ECDSAAlgorithm("ES256", "SHA256withECDSA", 128, (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); - AlgorithmUtils.verify(algorithm, jwt); + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); + Algorithm algorithm = new ECDSAAlgorithm("ES256", "SHA256withECDSA", 128, provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -302,13 +461,19 @@ 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); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); + 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); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -318,13 +483,19 @@ 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); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); + 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); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -334,85 +505,221 @@ 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); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); + 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); String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @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 + //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")); - byte[] contentBytes = String.format("%s.%s", ES256Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); + + 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")); + byte[] signatureBytes = algorithm.sign(ES256HeaderBytes, auth0IssPayloadBytes); + String jwtSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes); + String jwt = String.format("%s.%s.%s", ES256Header, auth0IssPayload, jwtSignature); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } - assertThat(signatureBytes, is(notNullValue())); - algorithmVerify.verify(contentBytes, signatureBytes); + @Test + public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA256(provider); + + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + 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], new byte[0]); } @Test public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not a ECPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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")); - byte[] contentBytes = String.format("%s.%s", ES384Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); + String jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); + + 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 jwt = asJWT(algorithm, ES384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA384(provider); + + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); - assertThat(signatureBytes, is(notNullValue())); - algorithmVerify.verify(contentBytes, signatureBytes); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA384withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not a ECPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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")); - byte[] contentBytes = String.format("%s.%s", ES512Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - assertThat(signatureBytes, is(notNullValue())); - algorithmVerify.verify(contentBytes, signatureBytes); + 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 jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + + @Test + public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA512(provider); + + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA512withECDSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not a ECPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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 @@ -422,12 +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); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); - algorithm.sign(ES256Header.getBytes()); + 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(ES256HeaderBytes, new byte[0]); } @Test @@ -437,42 +746,607 @@ 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); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + 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(ES256HeaderBytes, new byte[0]); } @Test - public void shouldThrowOnSignWhenUsingPublicKey() throws Exception { + public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: some-algorithm"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given ECKey is not a ECPrivateKey."))); + exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) - .thenThrow(InvalidKeyException.class); + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) + .thenThrow(SignatureException.class); - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPublicKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + 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(ES256HeaderBytes, new byte[0]); } @Test - public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); + Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); + + assertThat(algorithm.getSigningKeyId(), is(nullValue())); + } + + @Test + public void shouldReturnSigningKeyIdFromProvider() { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("keyId"); + Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); + + assertThat(algorithm.getSigningKeyId(), is("keyId")); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequenceByte() throws Exception { + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + 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(), new byte[0]); + signature[0] = (byte) 0x02; + algorithm256.DERToJOSE(signature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + int received = (int) derSignature[1]; + received--; + derSignature[1] = (byte) received; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfRNumberDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + derSignature[3] = (byte) 34; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfSNumberDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + derSignature[4 + 32 + 1] = (byte) 34; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() throws Exception { + 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.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 header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm256, header256, body); + algorithm256.verify(JWT.decode(jwt)); + } + } + + @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 header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm384, header384, body); + algorithm384.verify(JWT.decode(jwt)); + } + } + + @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 header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm512, header512, body); + algorithm512.verify(JWT.decode(jwt)); + } + } + + @Test + public void shouldDecodeECDSA256JOSE() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(32, false, false); + byte[] derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, false, false); + + //With R padding + joseSignature = createJOSESignature(32, true, false); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, true, false); + + //With S padding + joseSignature = createJOSESignature(32, false, true); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, false, true); + + //With both paddings + joseSignature = createJOSESignature(32, true, true); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, true, true); + } + + @Test + public void shouldDecodeECDSA256DER() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(32, false, false); + byte[] joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, false, false); + + //With R padding + derSignature = createDERSignature(32, true, false); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, true, false); + + //With S padding + derSignature = createDERSignature(32, false, true); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, false, true); + + //With both paddings + derSignature = createDERSignature(32, true, true); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, true, true); + } + + @Test + public void shouldDecodeECDSA384JOSE() throws Exception { + ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(48, false, false); + byte[] derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, false, false); + + //With R padding + joseSignature = createJOSESignature(48, true, false); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, true, false); + + //With S padding + joseSignature = createJOSESignature(48, false, true); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, false, true); + + //With both paddings + joseSignature = createJOSESignature(48, true, true); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, true, true); + } + + @Test + public void shouldDecodeECDSA384DER() throws Exception { + ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(48, false, false); + byte[] joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, false, false); + + //With R padding + derSignature = createDERSignature(48, true, false); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, true, false); + + //With S padding + derSignature = createDERSignature(48, false, true); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, false, true); + + //With both paddings + derSignature = createDERSignature(48, true, true); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, true, true); + } + + @Test + public void shouldDecodeECDSA512JOSE() throws Exception { + ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(66, false, false); + byte[] derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, false, false); + + //With R padding + joseSignature = createJOSESignature(66, true, false); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, true, false); + + //With S padding + joseSignature = createJOSESignature(66, false, true); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, false, true); + + //With both paddings + joseSignature = createJOSESignature(66, true, true); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, true, true); + } + + @Test + public void shouldDecodeECDSA512DER() throws Exception { + ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(66, false, false); + byte[] joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, false, false); + + //With R padding + derSignature = createDERSignature(66, true, false); + joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, true, false); + + //With S padding + derSignature = createDERSignature(66, false, true); + joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, false, true); + + //With both paddings + derSignature = createDERSignature(66, true, true); + joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, true, true); + } + + + //Test Helpers + static void assertValidJOSESignature(byte[] joseSignature, int numberSize, boolean withRPadding, boolean withSPadding) { + assertThat(joseSignature, is(Matchers.notNullValue())); + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + + assertThat(joseSignature.length, is(numberSize * 2)); + + byte[] rCopy = Arrays.copyOfRange(joseSignature, 0, numberSize); + byte[] sCopy = Arrays.copyOfRange(joseSignature, numberSize, numberSize * 2); + + byte[] rNumber = new byte[numberSize]; + byte[] sNumber = new byte[numberSize]; + Arrays.fill(rNumber, (byte) 0x11); + Arrays.fill(sNumber, (byte) 0x22); + if (withRPadding) { + rNumber[0] = (byte) 0; + } + if (withSPadding) { + sNumber[0] = (byte) 0; + } + assertThat(Arrays.equals(rNumber, rCopy), is(true)); + assertThat(Arrays.equals(sNumber, sCopy), is(true)); + } + + static byte[] createDERSignature(int numberSize, boolean withRPadding, boolean withSPadding) { + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + + int rLength = withRPadding ? numberSize - 1 : numberSize; + int sLength = withSPadding ? numberSize - 1 : numberSize; + int totalLength = 2 + (2 + rLength) + (2 + sLength); + + byte[] rNumber = new byte[rLength]; + byte[] sNumber = new byte[sLength]; + Arrays.fill(rNumber, (byte) 0x11); + Arrays.fill(sNumber, (byte) 0x22); + + byte[] derSignature; + int offset = 0; + if (totalLength > 0x7f) { + totalLength++; + derSignature = new byte[totalLength]; + //Start sequence and sign + derSignature[offset++] = (byte) 0x30; + derSignature[offset++] = (byte) 0x81; + } else { + derSignature = new byte[totalLength]; + //Start sequence + derSignature[offset++] = (byte) 0x30; + } + + //Sequence length + derSignature[offset++] = (byte) (totalLength - offset); + + //R number + derSignature[offset++] = (byte) 0x02; + derSignature[offset++] = (byte) rLength; + System.arraycopy(rNumber, 0, derSignature, offset, rLength); + offset += rLength; + + //S number + derSignature[offset++] = (byte) 0x02; + derSignature[offset++] = (byte) sLength; + System.arraycopy(sNumber, 0, derSignature, offset, sLength); + + return derSignature; + } + + static byte[] createJOSESignature(int numberSize, boolean withRPadding, boolean withSPadding) { + assertThat(numberSize, is(IsIn.oneOf(32, 48, 66))); + + byte[] rNumber = new byte[numberSize]; + byte[] sNumber = new byte[numberSize]; + Arrays.fill(rNumber, (byte) 0x11); + Arrays.fill(sNumber, (byte) 0x22); + if (withRPadding) { + rNumber[0] = (byte) 0; + } + if (withSPadding) { + sNumber[0] = (byte) 0; + } + byte[] joseSignature = new byte[numberSize * 2]; + System.arraycopy(rNumber, 0, joseSignature, 0, numberSize); + System.arraycopy(sNumber, 0, joseSignature, numberSize, numberSize); + return joseSignature; + } + + static void assertValidDERSignature(byte[] derSignature, int numberSize, boolean withRPadding, boolean withSPadding) { + 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; + int totalLength = 2 + (2 + rLength) + (2 + sLength); + int offset = 0; + + //Start sequence + assertThat(derSignature[offset++], is((byte) 0x30)); + if (totalLength > 0x7f) { + //Add sign before sequence length + totalLength++; + assertThat(derSignature[offset++], is((byte) 0x81)); + } + //Sequence length + assertThat(derSignature[offset++], is((byte) (totalLength - offset))); + + //R number + assertThat(derSignature[offset++], is((byte) 0x02)); + assertThat(derSignature[offset++], is((byte) rLength)); + byte[] rCopy = Arrays.copyOfRange(derSignature, offset, offset + rLength); + offset += rLength; + + //S number + assertThat(derSignature[offset++], is((byte) 0x02)); + assertThat(derSignature[offset++], is((byte) sLength)); + byte[] sCopy = Arrays.copyOfRange(derSignature, offset, offset + sLength); + + + byte[] rNumber = new byte[rLength]; + byte[] sNumber = new byte[sLength]; + Arrays.fill(rNumber, (byte) 0x11); + Arrays.fill(sNumber, (byte) 0x22); + 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: some-algorithm"); + 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)); - CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) - .thenThrow(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; - ECKey key = mock(ECKey.class, withSettings().extraInterfaces(ECPrivateKey.class)); - Algorithm algorithm = new ECDSAAlgorithm(crypto, "some-alg", "some-algorithm", 32, key); - algorithm.sign(ES256Header.getBytes(StandardCharsets.UTF_8)); + 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 new file mode 100644 index 00000000..3925eef3 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/algorithms/ECDSABouncyCastleProviderTests.java @@ -0,0 +1,1037 @@ +package com.auth0.jwt.algorithms; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.SignatureGenerationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +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.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; + +public class ECDSABouncyCastleProviderTests { + + 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"; + + 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(); + + //JOSE Signatures obtained using Node 'jwa' lib: https://github.com/brianloveswords/node-jwa + //DER Signatures obtained from source JOSE signature using 'ecdsa-sig-formatter' lib: https://github.com/Brightspace/node-ecdsa-sig-formatter + + + //These tests add and use the BouncyCastle SecurityProvider to handle ECDSA algorithms + + @BeforeClass + public static void setUp() { + //Set BC as the preferred bcProvider + Security.insertProviderAt(bcProvider, 1); + } + + @AfterClass + public static void tearDown() { + Security.removeProvider(bcProvider.getName()); + } + + @Test + public void shouldPreferBouncyCastleProvider() { + assertThat(Security.getProviders()[0], is(equalTo(bcProvider))); + } + + @Test + public void shouldPassECDSA256VerificationWithJOSESignature() throws Exception { + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + Algorithm algorithm = Algorithm.ECDSA256(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA256VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.MEYCIQDiJWTf5jShFPj0hpCWn7x1nhxPMjKWCs9MMusS9AIhAMcFPJVLe2A9uvb8hl8sRO2IpGoKDRpDmyH14ixNPAHW"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + Algorithm algorithm = Algorithm.ECDSA256(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA256VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + Algorithm algorithm = Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA256VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "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)); + } + + @Test + public void shouldPassECDSA256VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.D_oU4CB0ZEsxHOjcWnmS3ZJvlTzm6WcGFx-HASxnvcB2Xu2WjI-axqXH9xKq45aPBDs330JpRhJmqBSc2K8MXQ"; + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.D_oU4CB0ZEsxHOjcWnmS3ZJvlTzm6WcGFx-HASxnvcB2Xu2WjI-axqXH9xKq45aPBDs330JpRhJmqBSc2K8MXQ"; + Algorithm algorithm = Algorithm.ECDSA256(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256VerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.W9qfN1b80B9hnMo49WL8THrOsf1vEjOhapeFemPMGySzxTcgfyudS5esgeBTO908X5SLdAr5jMwPUPBs9b6nNg"; + Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_256, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256VerificationWhenUsingPrivateKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.W9qfN1b80B9hnMo49WL8THrOsf1vEjOhapeFemPMGySzxTcgfyudS5esgeBTO908X5SLdAr5jMwPUPBs9b6nNg"; + Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA256VerificationOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + byte[] bytes = new byte[63]; + new SecureRandom().nextBytes(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)); + } + + @Test + public void shouldFailECDSA256VerificationOnInvalidJOSESignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + new SecureRandom().nextBytes(bytes); + String signature = Base64.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)); + } + + @Test + public void shouldFailECDSA256VerificationOnInvalidDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + + byte[] bytes = new byte[64]; + bytes[0] = 0x30; + new SecureRandom().nextBytes(bytes); + String signature = Base64.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)); + } + + @Test + public void shouldPassECDSA384VerificationWithJOSESignature() throws Exception { + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + Algorithm algorithm = Algorithm.ECDSA384(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA384VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.MGUCMQDnRRTlUo10XXBKRjyNAEqm4dmh7ohkEmbk2gHxtH6GdGDq2L4IduahG2UtccCMH8CE2vHCTMuk3pzAtoOtxkB8rXPK2KF6m8LUuEdCqPwF2yxVJn8ZxpzAurDEv8w"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + Algorithm algorithm = Algorithm.ECDSA384(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA384VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9.50UU5VKNdF1wfykY8jQBKpvuHZoe6IZBJm5NvoB8bR-hnRg6ti-CHbmvoRtlLfnHfwITa_8cJMy6TenMC2g63GQHytc8rYoXqbwtS4R0Ko_AXbLFUmfxnGnMC6v4MS_z"; + Algorithm algorithm = Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA384VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + 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)); + } + + @Test + public void shouldPassECDSA384VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.9kjGuFTPx3ylfpqL0eY9H7TGmPepjQOBKI8UPoEvby6N7dDLF5HxLohosNxxFymNT7LzpeSgOPAB0wJEwG2Nl2ukgdUOpZOf492wog_i5ZcZmAykd3g1QH7onrzd69GU"; + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.9kjGuFTPx3ylfpqL0eY9H7TGmPepjQOBKI8UPoEvby6N7dDLF5HxLohosNxxFymNT7LzpeSgOPAB0wJEwG2Nl2ukgdUOpZOf492wog_i5ZcZmAykd3g1QH7onrzd69GU"; + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA384VerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9._k5h1KyO-NE0R2_HAw0-XEc0bGT5atv29SxHhOGC9JDqUHeUdptfCK_ljQ01nLVt2OQWT2SwGs-TuyHDFmhPmPGFZ9wboxvq_ieopmYqhQilNAu-WF-frioiRz9733fU"; + Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_384, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA384VerificationWhenUsingPrivateKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + String jwt = "eyJhbGciOiJFUzM4NCJ9.eyJpc3MiOiJhdXRoMCJ9._k5h1KyO-NE0R2_HAw0-XEc0bGT5atv29SxHhOGC9JDqUHeUdptfCK_ljQ01nLVt2OQWT2SwGs-TuyHDFmhPmPGFZ9wboxvq_ieopmYqhQilNAu-WF-frioiRz9733fU"; + Algorithm algorithm = Algorithm.ECDSA384((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA384VerificationOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + byte[] bytes = new byte[95]; + new SecureRandom().nextBytes(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)); + } + + @Test + public void shouldFailECDSA384VerificationOnInvalidJOSESignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + + byte[] bytes = new byte[96]; + new SecureRandom().nextBytes(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)); + } + + @Test + public void shouldFailECDSA384VerificationOnInvalidDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withECDSA"); + + byte[] bytes = new byte[96]; + new SecureRandom().nextBytes(bytes); + bytes[0] = 0x30; + 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)); + } + + @Test + public void shouldPassECDSA512VerificationWithJOSESignature() throws Exception { + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AeCJPDIsSHhwRSGZCY6rspi8zekOw0K9qYMNridP1Fu9uhrA1QrG-EUxXlE06yvmh2R7Rz0aE7kxBwrnq8L8aOBCAYAsqhzPeUvyp8fXjjgs0Eto5I0mndE2QHlgcMSFASyjHbU8wD2Rq7ZNzGQ5b2MZfpv030WGUajT-aZYWFUJHVg2"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + Algorithm algorithm = Algorithm.ECDSA512(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnECDSA512VerificationWithDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.MIGIAkIB4Ik8MixIeHBFIZkJjquymLzN6Q7DQr2pgw2uJ0UW726GsDVCsb4RTFeUTTrKaHZHtHPRoTuTEHCuerwvxo4EICQgGALKocz3lL8qfH1444LNBLaOSNJp3RNkB5YHDEhQEsox21PMA9kau2TcxkOW9jGX6b9N9FhlGo0mmWFhVCR1YNg"; + ECKey key = (ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + Algorithm algorithm = Algorithm.ECDSA512(key); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassECDSA512VerificationWithJOSESignatureWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AeCJPDIsSHhwRSGZCY6rspi8zekOw0K9qYMNridP1Fu9uhrA1QrG-EUxXlE06yvmh2R7Rz0aE7kxBwrnq8L8aOBCAYAsqhzPeUvyp8fXjjgs0Eto5I0mndE2QHlgcMSFASyjHbU8wD2Rq7ZNzGQ5b2MZfpv030WGUajT-aZYWFUJHVg2"; + Algorithm algorithm = Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowECDSA512VerificationWithDERSignatureWithBothKeys() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + 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)); + } + + @Test + public void shouldPassECDSA512VerificationWithProvidedPublicKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((ECPublicKey) publicKey); + String jwt = "eyJhbGciOiJFUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.AGxEwbsYa2bQ7Y7DAcTQnVD8PmLSlhJ20jg2OfdyPnqdXI8SgBaG6lGciq3_pofFhs1HEoFoJ33Jcluha24oMHIvAfwu8qbv_Wq3L2eI9Q0L0p6ul8Pd_BS8adRa2PgLc36xXGcRc7ID5YH-CYaQfsTp5YIaF0Po3h0QyCoQ6ZiYQkqm"; + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJFUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.AGxEwbsYa2bQ7Y7DAcTQnVD8PmLSlhJ20jg2OfdyPnqdXI8SgBaG6lGciq3_pofFhs1HEoFoJ33Jcluha24oMHIvAfwu8qbv_Wq3L2eI9Q0L0p6ul8Pd_BS8adRa2PgLc36xXGcRc7ID5YH-CYaQfsTp5YIaF0Po3h0QyCoQ6ZiYQkqm"; + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA512VerificationWithInvalidPublicKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AZgdopFFsN0amCSs2kOucXdpylD31DEm5ChK1PG0_gq5Mf47MrvVph8zHSVuvcrXzcE1U3VxeCg89mYW1H33Y-8iAF0QFkdfTUQIWKNObH543WNMYYssv3OtOj0znPv8atDbaF8DMYAtcT1qdmaSJRhx-egRE9HGZkinPh9CfLLLt58X"; + Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE_512, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA512VerificationWhenUsingPrivateKey() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + String jwt = "eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJhdXRoMCJ9.AZgdopFFsN0amCSs2kOucXdpylD31DEm5ChK1PG0_gq5Mf47MrvVph8zHSVuvcrXzcE1U3VxeCg89mYW1H33Y-8iAF0QFkdfTUQIWKNObH543WNMYYssv3OtOj0znPv8atDbaF8DMYAtcT1qdmaSJRhx-egRE9HGZkinPh9CfLLLt58X"; + Algorithm algorithm = Algorithm.ECDSA512((ECKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldFailECDSA512VerificationOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + byte[] bytes = new byte[131]; + new SecureRandom().nextBytes(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)); + } + + @Test + public void shouldFailECDSA512VerificationOnInvalidJOSESignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + + byte[] bytes = new byte[132]; + new SecureRandom().nextBytes(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)); + } + + @Test + public void shouldFailECDSA512VerificationOnInvalidDERSignature() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withECDSA"); + + byte[] bytes = new byte[132]; + new SecureRandom().nextBytes(bytes); + bytes[0] = 0x30; + 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)); + } + + @Test + public void shouldFailJOSEToDERConversionOnInvalidJOSESignatureLength() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(SignatureException.class)); + exception.expectCause(hasMessage(is("Invalid JOSE signature format."))); + + byte[] bytes = new byte[256]; + new SecureRandom().nextBytes(bytes); + String signature = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9." + signature; + + ECPublicKey publicKey = (ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); + Algorithm algorithm = new ECDSAAlgorithm("ES256", "SHA256withECDSA", 128, provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnVerifyWhenSignatureAlgorithmDoesNotExists() 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(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); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnVerifyWhenThePublicKeyIsInvalid() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: some-alg"); + exception.expectCause(isA(InvalidKeyException.class)); + + CryptoHelper crypto = mock(CryptoHelper.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); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldThrowOnVerifyWhenTheSignatureIsNotPrepared() throws Exception { + exception.expect(SignatureVerificationException.class); + exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: some-alg"); + exception.expectCause(isA(SignatureException.class)); + + CryptoHelper crypto = mock(CryptoHelper.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); + String jwt = "eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJhdXRoMCJ9.4iVk3-Y0v4RT4_9IaQlp-8dZ_4fsTzIylgrPTDLrEvTHBTyVS3tgPbr2_IZfLETtiKRqCg0aQ5sh9eIsTTwB1g"; + 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"; + + @Test + 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 jwt = asJWT(algorithmSign, ES256Header, auth0IssPayload); + + 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 jwt = asJWT(algorithm, ES256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoECDSA256SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA256(provider); + + String jwt = asJWT(algorithm, ES256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + 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], new byte[0]); + } + + @Test + public void shouldFailOnECDSA256SigningWhenUsingPublicKey() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withECDSA"); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + Algorithm algorithm = Algorithm.ECDSA256((ECKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC")); + 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 jwt = asJWT(algorithmSign, ES384Header, auth0IssPayload); + + 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 jwt = asJWT(algorithm, ES384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoECDSA384SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA384(provider); + + String jwt = asJWT(algorithm, ES384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA384(provider); + algorithm.sign(new byte[0], new byte[0]); + } + + @Test + public void shouldFailOnECDSA384SigningWhenUsingPublicKey() throws Exception { + 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)); + 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], 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 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 jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + + @Test + public void shouldDoECDSA512SigningWithProvidedPrivateKey() throws Exception { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"); + when(provider.getPrivateKey()).thenReturn((ECPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((ECPublicKey) publicKey); + Algorithm algorithm = Algorithm.ECDSA512(provider); + String jwt = asJWT(algorithm, ES512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.ECDSA512(provider); + algorithm.sign(new byte[0], new byte[0]); + } + + @Test + public void shouldFailOnECDSA512SigningWhenUsingPublicKey() throws Exception { + 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)); + 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], new byte[0]); + } + + @Test + public void shouldThrowOnSignWhenSignatureAlgorithmDoesNotExists() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: some-algorithm"); + exception.expectCause(isA(NoSuchAlgorithmException.class)); + + CryptoHelper crypto = mock(CryptoHelper.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), new byte[0]); + } + + @Test + public void shouldThrowOnSignWhenThePrivateKeyIsInvalid() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: some-algorithm"); + exception.expectCause(isA(InvalidKeyException.class)); + + CryptoHelper crypto = mock(CryptoHelper.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), new byte[0]); + } + + @Test + public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { + exception.expect(SignatureGenerationException.class); + exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: some-algorithm"); + exception.expectCause(isA(SignatureException.class)); + + CryptoHelper crypto = mock(CryptoHelper.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), new byte[0]); + } + + @Test + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { + ECPublicKey publicKey = mock(ECPublicKey.class); + ECPrivateKey privateKey = mock(ECPrivateKey.class); + ECDSAKeyProvider provider = ECDSAAlgorithm.providerForKeys(publicKey, privateKey); + Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); + + assertThat(algorithm.getSigningKeyId(), is(nullValue())); + } + + @Test + public void shouldReturnSigningKeyIdFromProvider() { + ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class); + when(provider.getPrivateKeyId()).thenReturn("keyId"); + Algorithm algorithm = new ECDSAAlgorithm("some-alg", "some-algorithm", 32, provider); + + assertThat(algorithm.getSigningKeyId(), is("keyId")); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfDoesNotStartWithCorrectSequenceByte() throws Exception { + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + 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(), new byte[0]); + signature[0] = (byte) 0x02; + algorithm256.DERToJOSE(signature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + int received = (int) derSignature[1]; + received--; + derSignature[1] = (byte) received; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfRNumberDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + derSignature[3] = (byte) 34; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnDERSignatureConversionIfSNumberDoesNotHaveExpectedLength() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + byte[] derSignature = createDERSignature(32, false, false); + derSignature[4 + 32 + 1] = (byte) 34; + exception.expect(SignatureException.class); + exception.expectMessage("Invalid DER signature format."); + + algorithm256.DERToJOSE(derSignature); + } + + @Test + public void shouldThrowOnJOSESignatureConversionIfDoesNotHaveExpectedLength() throws Exception { + 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.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 header256 = "eyJhbGciOiJFUzI1NiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm256, header256, body); + algorithm256.verify(JWT.decode(jwt)); + } + } + + @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 header384 = "eyJhbGciOiJFUzM4NCJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm384, header384, body); + algorithm384.verify(JWT.decode(jwt)); + } + } + + @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 header512 = "eyJhbGciOiJFUzUxMiJ9"; + String body = "eyJpc3MiOiJhdXRoMCJ9"; + + for (int i = 0; i < 10; i++) { + String jwt = asJWT(algorithm512, header512, body); + algorithm512.verify(JWT.decode(jwt)); + } + } + + @Test + public void shouldDecodeECDSA256JOSE() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(32, false, false); + byte[] derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, false, false); + + //With R padding + joseSignature = createJOSESignature(32, true, false); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, true, false); + + //With S padding + joseSignature = createJOSESignature(32, false, true); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, false, true); + + //With both paddings + joseSignature = createJOSESignature(32, true, true); + derSignature = algorithm256.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 32, true, true); + } + + @Test + public void shouldDecodeECDSA256DER() throws Exception { + ECDSAAlgorithm algorithm256 = (ECDSAAlgorithm) Algorithm.ECDSA256((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_256, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(32, false, false); + byte[] joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, false, false); + + //With R padding + derSignature = createDERSignature(32, true, false); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, true, false); + + //With S padding + derSignature = createDERSignature(32, false, true); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, false, true); + + //With both paddings + derSignature = createDERSignature(32, true, true); + joseSignature = algorithm256.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 32, true, true); + } + + @Test + public void shouldDecodeECDSA384JOSE() throws Exception { + ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(48, false, false); + byte[] derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, false, false); + + //With R padding + joseSignature = createJOSESignature(48, true, false); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, true, false); + + //With S padding + joseSignature = createJOSESignature(48, false, true); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, false, true); + + //With both paddings + joseSignature = createJOSESignature(48, true, true); + derSignature = algorithm384.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 48, true, true); + } + + @Test + public void shouldDecodeECDSA384DER() throws Exception { + ECDSAAlgorithm algorithm384 = (ECDSAAlgorithm) Algorithm.ECDSA384((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_384, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_384, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(48, false, false); + byte[] joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, false, false); + + //With R padding + derSignature = createDERSignature(48, true, false); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, true, false); + + //With S padding + derSignature = createDERSignature(48, false, true); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, false, true); + + //With both paddings + derSignature = createDERSignature(48, true, true); + joseSignature = algorithm384.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 48, true, true); + } + + @Test + public void shouldDecodeECDSA512JOSE() throws Exception { + ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + + //Without padding + byte[] joseSignature = createJOSESignature(66, false, false); + byte[] derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, false, false); + + //With R padding + joseSignature = createJOSESignature(66, true, false); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, true, false); + + //With S padding + joseSignature = createJOSESignature(66, false, true); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, false, true); + + //With both paddings + joseSignature = createJOSESignature(66, true, true); + derSignature = algorithm512.JOSEToDER(joseSignature); + assertValidDERSignature(derSignature, 66, true, true); + } + + @Test + public void shouldDecodeECDSA512DER() throws Exception { + ECDSAAlgorithm algorithm512 = (ECDSAAlgorithm) Algorithm.ECDSA512((ECPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE_512, "EC"), (ECPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE_512, "EC")); + + //Without padding + byte[] derSignature = createDERSignature(66, false, false); + byte[] joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, false, false); + + //With R padding + derSignature = createDERSignature(66, true, false); + joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, true, false); + + //With S padding + derSignature = createDERSignature(66, false, true); + joseSignature = algorithm512.DERToJOSE(derSignature); + assertValidJOSESignature(joseSignature, 66, false, true); + + //With both paddings + derSignature = createDERSignature(66, true, true); + joseSignature = algorithm512.DERToJOSE(derSignature); + 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 1c8a2012..9b6ac0c0 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/HMACAlgorithmTest.java @@ -1,20 +1,25 @@ package com.auth0.jwt.algorithms; +import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; -import org.apache.commons.codec.binary.Base64; +import com.auth0.jwt.interfaces.DecodedJWT; 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; @@ -28,91 +33,106 @@ 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()); - AlgorithmUtils.verify(algorithmString, jwt); - AlgorithmUtils.verify(algorithmBytes, jwt); + Algorithm algorithmBytes = Algorithm.HMAC256("secret".getBytes(StandardCharsets.UTF_8)); + DecodedJWT decoded = JWT.decode(jwt); + algorithmString.verify(decoded); + algorithmBytes.verify(decoded); } @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"; Algorithm algorithm = Algorithm.HMAC256("not_real_secret"); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @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"; - Algorithm algorithm = Algorithm.HMAC256("not_real_secret".getBytes()); - AlgorithmUtils.verify(algorithm, jwt); + Algorithm algorithm = Algorithm.HMAC256("not_real_secret".getBytes(StandardCharsets.UTF_8)); + algorithm.verify(JWT.decode(jwt)); } @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()); - AlgorithmUtils.verify(algorithmString, jwt); - AlgorithmUtils.verify(algorithmBytes, jwt); + Algorithm algorithmBytes = Algorithm.HMAC384("secret".getBytes(StandardCharsets.UTF_8)); + DecodedJWT decoded = JWT.decode(jwt); + algorithmString.verify(decoded); + algorithmBytes.verify(decoded); } @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"; Algorithm algorithm = Algorithm.HMAC384("not_real_secret"); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @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"; - Algorithm algorithm = Algorithm.HMAC384("not_real_secret".getBytes()); - AlgorithmUtils.verify(algorithm, jwt); + Algorithm algorithm = Algorithm.HMAC384("not_real_secret".getBytes(StandardCharsets.UTF_8)); + algorithm.verify(JWT.decode(jwt)); } @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()); - AlgorithmUtils.verify(algorithmString, jwt); - AlgorithmUtils.verify(algorithmBytes, jwt); + Algorithm algorithmBytes = Algorithm.HMAC512("secret".getBytes(StandardCharsets.UTF_8)); + DecodedJWT decoded = JWT.decode(jwt); + algorithmString.verify(decoded); + algorithmBytes.verify(decoded); } @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"; Algorithm algorithm = Algorithm.HMAC512("not_real_secret"); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @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"; - Algorithm algorithm = Algorithm.HMAC512("not_real_secret".getBytes()); - AlgorithmUtils.verify(algorithm, jwt); + Algorithm algorithm = Algorithm.HMAC512("not_real_secret".getBytes(StandardCharsets.UTF_8)); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -122,12 +142,12 @@ 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()); + Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -137,12 +157,12 @@ 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()); + Algorithm algorithm = new HMACAlgorithm(crypto, "some-alg", "some-algorithm", "secret".getBytes(StandardCharsets.UTF_8)); String jwt = "eyJhbGciOiJIUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mZ0m_N1J4PgeqWmi903JuUoDRZDBPB7HwkS4nVyWH1M"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } // Sign @@ -153,81 +173,75 @@ 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)); - byte[] contentBytes = String.format("%s.%s", HS256Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + 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)); - byte[] contentBytes = String.format("%s.%s", HS384Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + 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)); - byte[] contentBytes = String.format("%s.%s", HS512Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm, HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC256SigningWithString() throws Exception { + public void shouldDoHMAC256SigningWithString() { Algorithm algorithm = Algorithm.HMAC256("secret"); - byte[] contentBytes = String.format("%s.%s", HS256Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm, HS256Header, auth0IssPayload); String expectedSignature = "s69x7Mmu4JqwmdxiK6sesALO7tcedbFsKEEITUxw9ho"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC384SigningWithString() throws Exception { + public void shouldDoHMAC384SigningWithString() { Algorithm algorithm = Algorithm.HMAC384("secret"); - byte[] contentBytes = String.format("%s.%s", HS384Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm, HS384Header, auth0IssPayload); String expectedSignature = "4-y2Gxz_foN0jAOFimmBPF7DWxf4AsjM20zxNkHg8Zah5Q64G42P9GfjmUp4Hldt"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); } @Test - public void shouldDoHMAC512SigningWithString() throws Exception { + public void shouldDoHMAC512SigningWithString() { Algorithm algorithm = Algorithm.HMAC512("secret"); - byte[] contentBytes = String.format("%s.%s", HS512Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithm.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + + String jwt = asJWT(algorithm ,HS512Header, auth0IssPayload); String expectedSignature = "OXWyxmf-VcVo8viOiTFfLaEy6mrQqLEos5R82Xsx8mtFxQadJAQ1aVniIWN8qT2GNE_pMQPcdzk4x7Cqxsp1dw"; - assertThat(signatureBytes, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithm.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -237,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 @@ -251,11 +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() { + 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 18794640..263dc249 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/NoneAlgorithmTest.java @@ -1,29 +1,57 @@ package com.auth0.jwt.algorithms; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.SignatureVerificationException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + public class NoneAlgorithmTest { @Rule 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() { + exception.expect(JWTDecodeException.class); + exception.expectMessage("The token was expected to have 3 parts, but got 2."); String jwt = "eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJpc3MiOiJhdXRoMCJ9"; - AlgorithmUtils.verify(algorithm, jwt); + Algorithm algorithm = Algorithm.none(); + algorithm.verify(JWT.decode(jwt)); } @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"; Algorithm algorithm = Algorithm.none(); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } + @Test + 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 ea6961fc..115de64c 100644 --- a/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java +++ b/lib/src/test/java/com/auth0/jwt/algorithms/RSAAlgorithmTest.java @@ -1,13 +1,14 @@ package com.auth0.jwt.algorithms; +import com.auth0.jwt.JWT; import com.auth0.jwt.exceptions.SignatureGenerationException; import com.auth0.jwt.exceptions.SignatureVerificationException; -import org.apache.commons.codec.binary.Base64; +import com.auth0.jwt.interfaces.RSAKeyProvider; 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; @@ -16,12 +17,13 @@ import static com.auth0.jwt.PemUtils.readPrivateKeyFromFile; import static com.auth0.jwt.PemUtils.readPublicKeyFromFile; import static org.hamcrest.Matchers.*; -import static org.hamcrest.Matchers.isA; -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.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static com.auth0.jwt.algorithms.CryptoTestHelper.*; public class RSAAlgorithmTest { @@ -38,7 +40,37 @@ public class RSAAlgorithmTest { public void shouldPassRSA256Verification() throws Exception { String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; Algorithm algorithm = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA256VerificationWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA256VerificationWithProvidedPublicKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((RSAPublicKey) publicKey); + String jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.jXrbue3xJmnzWH9kU-uGeCTtgbQEKbch8uHd4Z52t86ncNyepfusl_bsyLJIcxMwK7odRzKiSE9efV9JaRSEDODDBdMeCzODFx82uBM7e46T1NLVSmjYIM7Hcfh81ZeTIk-hITvgtL6hvTdeJWOCZAB0bs18qSVW5SvursRUhY38xnhuNI6HOHCtqp7etxWAu6670L53I3GtXsmi6bXIzv_0v1xZcAFg4HTvXxfhfj3oCqkSs2nC27mHxBmQtmZKWmXk5HzVUyPRwTUWx5wHPT_hCsGer-CMCAyGsmOg466y1KDqf7ogpMYojfVZGWBsyA39LO1oWZ4Ryomkn8t5Vg"; + Algorithm algorithm = Algorithm.RSA256(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.jXrbue3xJmnzWH9kU-uGeCTtgbQEKbch8uHd4Z52t86ncNyepfusl_bsyLJIcxMwK7odRzKiSE9efV9JaRSEDODDBdMeCzODFx82uBM7e46T1NLVSmjYIM7Hcfh81ZeTIk-hITvgtL6hvTdeJWOCZAB0bs18qSVW5SvursRUhY38xnhuNI6HOHCtqp7etxWAu6670L53I3GtXsmi6bXIzv_0v1xZcAFg4HTvXxfhfj3oCqkSs2nC27mHxBmQtmZKWmXk5HzVUyPRwTUWx5wHPT_hCsGer-CMCAyGsmOg466y1KDqf7ogpMYojfVZGWBsyA39LO1oWZ4Ryomkn8t5Vg"; + Algorithm algorithm = Algorithm.RSA256(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -47,25 +79,55 @@ public void shouldFailRSA256VerificationWithInvalidPublicKey() throws Exception exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA"); String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; Algorithm algorithm = Algorithm.RSA256((RSAKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailRSA256VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA256withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; Algorithm algorithm = Algorithm.RSA256((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldPassRSA384Verification() throws Exception { String jwt = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.TZlWjXObwGSQOiu2oMq8kiKz0_BR7bbBddNL6G8eZ_GoR82BXOZDqNrQr7lb_M-78XGBguWLWNIdYhzgxOUL9EoCJlrqVm9s9vo6G8T1sj1op-4TbjXZ61TwIvrJee9BvPLdKUJ9_fp1Js5kl6yXkst40Th8Auc5as4n49MLkipjpEhKDKaENKHpSubs1ripSz8SCQZSofeTM_EWVwSw7cpiM8Fy8jOPvWG8Xz4-e3ODFowvHVsDcONX_4FTMNbeRqDuHq2ZhCJnEfzcSJdrve_5VD5fM1LperBVslTrOxIgClOJ3RmM7-WnaizJrWP3D6Z9OLxPxLhM6-jx6tcxEw"; Algorithm algorithm = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA384VerificationWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.TZlWjXObwGSQOiu2oMq8kiKz0_BR7bbBddNL6G8eZ_GoR82BXOZDqNrQr7lb_M-78XGBguWLWNIdYhzgxOUL9EoCJlrqVm9s9vo6G8T1sj1op-4TbjXZ61TwIvrJee9BvPLdKUJ9_fp1Js5kl6yXkst40Th8Auc5as4n49MLkipjpEhKDKaENKHpSubs1ripSz8SCQZSofeTM_EWVwSw7cpiM8Fy8jOPvWG8Xz4-e3ODFowvHVsDcONX_4FTMNbeRqDuHq2ZhCJnEfzcSJdrve_5VD5fM1LperBVslTrOxIgClOJ3RmM7-WnaizJrWP3D6Z9OLxPxLhM6-jx6tcxEw"; + Algorithm algorithm = Algorithm.RSA384((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA384VerificationWithProvidedPublicKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((RSAPublicKey) publicKey); + String jwt = "eyJhbGciOiJSUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.ITNTVCT7ercumZKHV4-BXGkJwwa7fyF3CnSfEvm09fDFSkaseDxNo_75WLDmK9WM8RMHTPvkpHcTKm4guYEbC_la7RzFIKpU72bppzQojggSmWWXt_6zq50QP2t5HFMebote1zxhp8ccEdSCX5pyY6J2sm9kJ__HKK32KxIVCTjVCz-bFBS60oG35aYEySdKsxuUdWbD5FQ9I16Ony2x0EPvmlL3GPiAPmgjSFp3LtcBIbCDaoonM7iuDRGIQiDN_n2FKKb1Bt4_38uWPtTkwRpNalt6l53Y3JDdzGI5fMrMo3RQnQlAJxUJKD0eL6dRAA645IVIIXucHwuhgGGIVw"; + Algorithm algorithm = Algorithm.RSA384(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJSUzM4NCIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.ITNTVCT7ercumZKHV4-BXGkJwwa7fyF3CnSfEvm09fDFSkaseDxNo_75WLDmK9WM8RMHTPvkpHcTKm4guYEbC_la7RzFIKpU72bppzQojggSmWWXt_6zq50QP2t5HFMebote1zxhp8ccEdSCX5pyY6J2sm9kJ__HKK32KxIVCTjVCz-bFBS60oG35aYEySdKsxuUdWbD5FQ9I16Ony2x0EPvmlL3GPiAPmgjSFp3LtcBIbCDaoonM7iuDRGIQiDN_n2FKKb1Bt4_38uWPtTkwRpNalt6l53Y3JDdzGI5fMrMo3RQnQlAJxUJKD0eL6dRAA645IVIIXucHwuhgGGIVw"; + Algorithm algorithm = Algorithm.RSA384(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -74,25 +136,55 @@ public void shouldFailRSA384VerificationWithInvalidPublicKey() throws Exception exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withRSA"); String jwt = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.TZlWjXObwGSQOiu2oMq8kiKz0_BR7bbBddNL6G8eZ_GoR82BXOZDqNrQr7lb_M-78XGBguWLWNIdYhzgxOUL9EoCJlrqVm9s9vo6G8T1sj1op-4TbjXZ61TwIvrJee9BvPLdKUJ9_fp1Js5kl6yXkst40Th8Auc5as4n49MLkipjpEhKDKaENKHpSubs1ripSz8SCQZSofeTM_EWVwSw7cpiM8Fy8jOPvWG8Xz4-e3ODFowvHVsDcONX_4FTMNbeRqDuHq2ZhCJnEfzcSJdrve_5VD5fM1LperBVslTrOxIgClOJ3RmM7-WnaizJrWP3D6Z9OLxPxLhM6-jx6tcxEw"; Algorithm algorithm = Algorithm.RSA384((RSAKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailRSA384VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA384withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.TZlWjXObwGSQOiu2oMq8kiKz0_BR7bbBddNL6G8eZ_GoR82BXOZDqNrQr7lb_M-78XGBguWLWNIdYhzgxOUL9EoCJlrqVm9s9vo6G8T1sj1op-4TbjXZ61TwIvrJee9BvPLdKUJ9_fp1Js5kl6yXkst40Th8Auc5as4n49MLkipjpEhKDKaENKHpSubs1ripSz8SCQZSofeTM_EWVwSw7cpiM8Fy8jOPvWG8Xz4-e3ODFowvHVsDcONX_4FTMNbeRqDuHq2ZhCJnEfzcSJdrve_5VD5fM1LperBVslTrOxIgClOJ3RmM7-WnaizJrWP3D6Z9OLxPxLhM6-jx6tcxEw"; Algorithm algorithm = Algorithm.RSA384((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldPassRSA512Verification() throws Exception { String jwt = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; Algorithm algorithm = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA512VerificationWithBothKeys() throws Exception { + String jwt = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; + Algorithm algorithm = Algorithm.RSA512((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldPassRSA512VerificationWithProvidedPublicKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPublicKeyById("my-key-id")).thenReturn((RSAPublicKey) publicKey); + String jwt = "eyJhbGciOiJSUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.GpHv85Q8tAU_6hNWsmO0GEpO1qz9lmK3NKeAcemysz9MGo4FXWn8xbD8NjCfzZ8EWphm65M0NArKSjpKHO5-gcNsQxLBVfSED1vzcoaZH_Vy5Rp1M76dGH7JghB_66KrpfyMxer_yRJb-KXesNvIroDGilLQF2ENG-IfLF5nBKlDiVHmPaqr3pm1q20fNLhegkSRca4BJ5VdIlT6kOqE_ykVyCBqzD_oXp3LKO_ARnxoeB9SegIW1fy_3tuxSTKYsCZiOfiyVEXXblAuY3pSLZnGvgeBRnfvmWXDWhP0vVUFtYJBF09eULvvUMVqWcrjUG9gDzzzT7veiY_fHd_x8g"; + Algorithm algorithm = Algorithm.RSA512(provider); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPublicKeyById("my-key-id")).thenReturn(null); + String jwt = "eyJhbGciOiJSUzUxMiIsImtpZCI6Im15LWtleS1pZCJ9.eyJpc3MiOiJhdXRoMCJ9.GpHv85Q8tAU_6hNWsmO0GEpO1qz9lmK3NKeAcemysz9MGo4FXWn8xbD8NjCfzZ8EWphm65M0NArKSjpKHO5-gcNsQxLBVfSED1vzcoaZH_Vy5Rp1M76dGH7JghB_66KrpfyMxer_yRJb-KXesNvIroDGilLQF2ENG-IfLF5nBKlDiVHmPaqr3pm1q20fNLhegkSRca4BJ5VdIlT6kOqE_ykVyCBqzD_oXp3LKO_ARnxoeB9SegIW1fy_3tuxSTKYsCZiOfiyVEXXblAuY3pSLZnGvgeBRnfvmWXDWhP0vVUFtYJBF09eULvvUMVqWcrjUG9gDzzzT7veiY_fHd_x8g"; + Algorithm algorithm = Algorithm.RSA512(provider); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -101,18 +193,18 @@ public void shouldFailRSA512VerificationWithInvalidPublicKey() throws Exception exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withRSA"); String jwt = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; Algorithm algorithm = Algorithm.RSA512((RSAKey) readPublicKeyFromFile(INVALID_PUBLIC_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test public void shouldFailRSA512VerificationWhenUsingPrivateKey() throws Exception { exception.expect(SignatureVerificationException.class); exception.expectMessage("The Token's Signature resulted invalid when verified using the Algorithm: SHA512withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPublicKey."))); + exception.expectCause(isA(IllegalStateException.class)); + exception.expectCause(hasMessage(is("The given Public Key is null."))); String jwt = "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.mvL5LoMyIrWYjk5umEXZTmbyIrkbbcVPUkvdGZbu0qFBxGOf0nXP5PZBvPcOu084lvpwVox5n3VaD4iqzW-PsJyvKFgi5TnwmsbKchAp7JexQEsQOnTSGcfRqeUUiBZqRQdYsho71oAB3T4FnalDdFEpM-fztcZY9XqKyayqZLreTeBjqJm4jfOWH7KfGBHgZExQhe96NLq1UA9eUyQwdOA1Z0SgXe4Ja5PxZ6Fm37KnVDtDlNnY4JAAGFo6y74aGNnp_BKgpaVJCGFu1f1S5xCQ1HSvs8ZSdVWs5NgawW3wRd0kRt_GJ_Y3mIwiF4qUyHWGtsSHu_qjVdCTtbFyow"; Algorithm algorithm = Algorithm.RSA512((RSAKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -120,15 +212,17 @@ 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); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); + 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); String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -138,13 +232,15 @@ 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); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); + 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); String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @Test @@ -154,13 +250,15 @@ 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); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); + 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); String jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoMCJ9.dxXF3MdsyW-AuvwJpaQtrZ33fAde9xWxpLIg9cO2tMLH2GSRNuLAe61KsJusZhqZB9Iy7DvflcmRz-9OZndm6cj_ThGeJH2LLc90K83UEvvRPo8l85RrQb8PcanxCgIs2RcZOLygERizB3pr5icGkzR7R2y6zgNCjKJ5_NJ6EiZsGN6_nc2PRK_DbyY-Wn0QDxIxKoA5YgQJ9qafe7IN980pXvQv2Z62c3XR8dYuaXBqhthBj-AbaFHEpZapN-V-TmuLNzR2MCB6Xr7BYMuCaqWf_XU8og4XNe8f_8w9Wv5vvgqMM1KhqVpG5VdMJv4o_L4NoCROHhtUQSLRh2M9cA"; - AlgorithmUtils.verify(algorithm, jwt); + algorithm.verify(JWT.decode(jwt)); } @@ -175,25 +273,63 @@ 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")); - byte[] contentBytes = String.format("%s.%s", RS256Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + String jwt = asJWT(algorithmSign, RS256Header, auth0IssPayload); + String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; + + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithmVerify.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA256SigningWithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); String expectedSignature = "ZB-Tr0vLtnf8I9fhSdSjU6HZei5xLYZQ6nZqM5O6Va0W9PgAqgRT7ShI9CjeYulRXPHvVmSl5EQuYuXdBzM0-H_3p_Nsl6tSMy4EyX2kkhEm6T0HhvarTh8CG0PCjn5p6FP5ZxWwhLcmRN70ItP6Z5MMO4CcJh1JrNxR4Fi4xQgt-CK2aVDMFXd-Br5yQiLVx1CX83w28OD9wssW3Rdltl5e66vCef0Ql6Q5I5e5F0nqGYT989a9fkNgLIx2F8k_az5x07BY59FV2SZg59nSiY7TZNjP8ot11Ew7HKRfPXOdh9eKRUVdhcxzqDePhyzKabU8TG5FP0SiWH5qVPfAgw"; - assertThat(signature, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithmVerify.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA256SigningWithProvidedPrivateKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); + Algorithm algorithm = Algorithm.RSA256(provider); + + String jwt = asJWT(algorithm, RS256Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + 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], new byte[0]); } @Test public void shouldFailOnRSA256SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA256withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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 @@ -201,25 +337,63 @@ 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")); - byte[] contentBytes = String.format("%s.%s", RS384Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + String jwt = asJWT(algorithmSign, RS384Header, auth0IssPayload); + String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; + + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithmVerify.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA384SigningWithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA384((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); String expectedSignature = "Jx1PaTBnjd_U56MNjifFcY7w9ImDbseg0y8Ijr2pSiA1_wzQb_wy9undaWfzR5YqdIAXvjS8AGuZUAzIoTG4KMgOgdVyYDz3l2jzj6wI-lgqfR5hTy1w1ruMUQ4_wobpdxAiJ4fEbg8Mi_GljOiCO-P1HilxKnpiOJZidR8MQGwTInsf71tOUkK4x5UsdmUueuZbaU-CL5kPnRfXmJj9CcdxZbD9oMlbo23dwkP5BNMrS2LwGGzc9C_-ypxrBIOVilG3WZxcSmuG86LjcZbnL6LBEfph5NmKBgQav147uipb_7umBEr1m2dYiB_9u606n3bcoo3rnsYYK_Xfi1GAEQ"; - assertThat(signature, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithmVerify.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA384SigningWithProvidedPrivateKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); + Algorithm algorithm = Algorithm.RSA384(provider); + + String jwt = asJWT(algorithm, RS384Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.RSA384(provider); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldFailOnRSA384SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA384withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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 @@ -227,25 +401,63 @@ 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")); - byte[] contentBytes = String.format("%s.%s", RS512Header, auth0IssPayload).getBytes(StandardCharsets.UTF_8); - byte[] signatureBytes = algorithmSign.sign(contentBytes); - String signature = Base64.encodeBase64URLSafeString(signatureBytes); + String jwt = asJWT(algorithmSign, RS512Header, auth0IssPayload); String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; - assertThat(signature, is(notNullValue())); - assertThat(signature, is(expectedSignature)); - algorithmVerify.verify(contentBytes, signatureBytes); + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithmVerify.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA512SigningWithBothKeys() throws Exception { + Algorithm algorithm = Algorithm.RSA512((RSAPublicKey) readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"), (RSAPrivateKey) readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA")); + + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); + String expectedSignature = "THIPVYzNZ1Yo_dm0k1UELqV0txs3SzyMopCyHcLXOOdgYXF4MlGvBqu0CFvgSga72Sp5LpuC1Oesj40v_QDsp2GTGDeWnvvcv_eo-b0LPSpmT2h1Ibrmu-z70u2rKf28pkN-AJiMFqi8sit2kMIp1bwIVOovPvMTQKGFmova4Xwb3G526y_PeLlflW1h69hQTIVcI67ACEkAC-byjDnnYIklA-B4GWcggEoFwQRTdRjAUpifA6HOlvnBbZZlUd6KXwEydxVS-eh1odwPjB2_sfbyy5HnLsvNdaniiZQwX7QbwLNT4F72LctYdHHM1QCrID6bgfgYp9Ij9CRX__XDEA"; + + assertSignaturePresent(jwt); + assertSignatureValue(jwt, expectedSignature); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + public void shouldDoRSA512SigningWithProvidedPrivateKey() throws Exception { + RSAKeyProvider provider = mock(RSAKeyProvider.class); + PrivateKey privateKey = readPrivateKeyFromFile(PRIVATE_KEY_FILE, "RSA"); + PublicKey publicKey = readPublicKeyFromFile(PUBLIC_KEY_FILE, "RSA"); + when(provider.getPrivateKey()).thenReturn((RSAPrivateKey) privateKey); + when(provider.getPublicKeyById(null)).thenReturn((RSAPublicKey) publicKey); + Algorithm algorithm = Algorithm.RSA512(provider); + + String jwt = asJWT(algorithm, RS512Header, auth0IssPayload); + + assertSignaturePresent(jwt); + algorithm.verify(JWT.decode(jwt)); + } + + @Test + 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)); + exception.expectCause(hasMessage(is("The given Private Key is null."))); + + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.RSA512(provider); + algorithm.sign(new byte[0], new byte[0]); } @Test public void shouldFailOnRSA512SigningWhenUsingPublicKey() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: SHA512withRSA"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPrivateKey."))); + exception.expectCause(isA(IllegalStateException.class)); + 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 @@ -255,12 +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); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + 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(new byte[0], new byte[0]); } @Test @@ -270,42 +484,96 @@ 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); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + 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(new byte[0], new byte[0]); } @Test - public void shouldThrowOnSignWhenUsingPublicKey() throws Exception { + public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { exception.expect(SignatureGenerationException.class); exception.expectMessage("The Token's Signature couldn't be generated when signing using the Algorithm: some-algorithm"); - exception.expectCause(isA(IllegalArgumentException.class)); - exception.expectCause(hasMessage(is("The given RSAKey is not a RSAPrivateKey."))); + exception.expectCause(isA(SignatureException.class)); CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) - .thenThrow(InvalidKeyException.class); + when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class), any(byte[].class))) + .thenThrow(SignatureException.class); - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPublicKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + 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(new byte[0], new byte[0]); } @Test - public void shouldThrowOnSignWhenTheSignatureIsNotPrepared() throws Exception { + public void shouldReturnNullSigningKeyIdIfCreatedWithDefaultProvider() { + RSAPublicKey publicKey = mock(RSAPublicKey.class); + RSAPrivateKey privateKey = mock(RSAPrivateKey.class); + RSAKeyProvider provider = RSAAlgorithm.providerForKeys(publicKey, privateKey); + Algorithm algorithm = new RSAAlgorithm("some-alg", "some-algorithm", provider); + + assertThat(algorithm.getSigningKeyId(), is(nullValue())); + } + + @Test + 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: some-algorithm"); - exception.expectCause(isA(SignatureException.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."))); - CryptoHelper crypto = mock(CryptoHelper.class); - when(crypto.createSignatureFor(anyString(), any(PrivateKey.class), any(byte[].class))) - .thenThrow(SignatureException.class); + RSAKeyProvider provider = mock(RSAKeyProvider.class); + when(provider.getPrivateKey()).thenReturn(null); + Algorithm algorithm = Algorithm.RSA256(provider); + algorithm.sign(new byte[0]); + } - RSAKey key = mock(RSAKey.class, withSettings().extraInterfaces(RSAPrivateKey.class)); - Algorithm algorithm = new RSAAlgorithm(crypto, "some-alg", "some-algorithm", key); - algorithm.sign(RS256Header.getBytes(StandardCharsets.UTF_8)); + @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/BaseClaimTest.java b/lib/src/test/java/com/auth0/jwt/impl/BaseClaimTest.java deleted file mode 100644 index bd4b5dec..00000000 --- a/lib/src/test/java/com/auth0/jwt/impl/BaseClaimTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.auth0.jwt.impl; - -import org.junit.Before; -import org.junit.Test; -import org.omg.CORBA.Object; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - -public class BaseClaimTest { - 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 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())); - } - -} \ No newline at end of file diff --git a/lib/src/test/java/com/auth0/jwt/impl/HeaderImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java similarity index 75% rename from lib/src/test/java/com/auth0/jwt/impl/HeaderImplTest.java rename to lib/src/test/java/com/auth0/jwt/impl/BasicHeaderTest.java index 653921f6..c4a04d81 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/HeaderImplTest.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; @@ -14,40 +16,42 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -public class HeaderImplTest { +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/ClaimImplTest.java b/lib/src/test/java/com/auth0/jwt/impl/ClaimImplTest.java deleted file mode 100644 index 5edaacb7..00000000 --- a/lib/src/test/java/com/auth0/jwt/impl/ClaimImplTest.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.auth0.jwt.impl; - -import com.auth0.jwt.UserPojo; -import com.auth0.jwt.exceptions.JWTDecodeException; -import com.auth0.jwt.interfaces.Claim; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.MissingNode; -import com.fasterxml.jackson.databind.node.NullNode; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import java.util.Arrays; -import java.util.Date; - -import static com.auth0.jwt.impl.JsonNodeClaim.claimFromNode; -import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; -import static org.hamcrest.Matchers.*; -import static org.hamcrest.core.IsNull.notNullValue; -import static org.hamcrest.core.IsNull.nullValue; -import static org.junit.Assert.assertThat; - -public class ClaimImplTest { - - private ObjectMapper mapper; - @Rule - public ExpectedException exception = ExpectedException.none(); - - @Before - public void setUp() throws Exception { - mapper = getDefaultObjectMapper(); - } - - @Test - public void shouldGetBooleanValue() throws Exception { - JsonNode value = mapper.valueToTree(true); - Claim claim = claimFromNode(value); - - assertThat(claim.asBoolean(), is(notNullValue())); - assertThat(claim.asBoolean(), is(true)); - } - - @Test - public void shouldGetNullBooleanIfNotBooleanValue() throws Exception { - JsonNode objectValue = mapper.valueToTree(new Object()); - assertThat(claimFromNode(objectValue).asBoolean(), is(nullValue())); - JsonNode stringValue = mapper.valueToTree("boolean"); - assertThat(claimFromNode(stringValue).asBoolean(), is(nullValue())); - } - - @Test - public void shouldGetIntValue() throws Exception { - JsonNode value = mapper.valueToTree(123); - Claim claim = claimFromNode(value); - - assertThat(claim.asInt(), is(notNullValue())); - assertThat(claim.asInt(), is(123)); - } - - @Test - public void shouldGetNullIntIfNotIntValue() throws Exception { - JsonNode objectValue = mapper.valueToTree(new Object()); - assertThat(claimFromNode(objectValue).asInt(), is(nullValue())); - JsonNode stringValue = mapper.valueToTree("123"); - assertThat(claimFromNode(stringValue).asInt(), is(nullValue())); - } - - @Test - public void shouldGetDoubleValue() throws Exception { - JsonNode value = mapper.valueToTree(1.5); - Claim claim = claimFromNode(value); - - assertThat(claim.asDouble(), is(notNullValue())); - assertThat(claim.asDouble(), is(1.5)); - } - - @Test - public void shouldGetNullDoubleIfNotDoubleValue() throws Exception { - JsonNode objectValue = mapper.valueToTree(new Object()); - assertThat(claimFromNode(objectValue).asDouble(), is(nullValue())); - JsonNode stringValue = mapper.valueToTree("123.23"); - assertThat(claimFromNode(stringValue).asDouble(), is(nullValue())); - } - - @Test - public void shouldGetDateValue() throws Exception { - JsonNode value = mapper.valueToTree(1476824844L); - Claim claim = claimFromNode(value); - - assertThat(claim.asDate(), is(notNullValue())); - assertThat(claim.asDate(), is(new Date(1476824844L * 1000))); - } - - @Test - public void shouldGetNullDateIfNotDateValue() throws Exception { - JsonNode objectValue = mapper.valueToTree(new Object()); - assertThat(claimFromNode(objectValue).asDate(), is(nullValue())); - JsonNode stringValue = mapper.valueToTree("1476824844"); - assertThat(claimFromNode(stringValue).asDate(), is(nullValue())); - } - - @Test - public void shouldGetStringValue() throws Exception { - JsonNode value = mapper.valueToTree("string"); - Claim claim = claimFromNode(value); - - assertThat(claim.asString(), is(notNullValue())); - assertThat(claim.asString(), is("string")); - } - - @Test - public void shouldGetNullStringIfNotStringValue() throws Exception { - JsonNode objectValue = mapper.valueToTree(new Object()); - assertThat(claimFromNode(objectValue).asString(), is(nullValue())); - JsonNode intValue = mapper.valueToTree(12345); - assertThat(claimFromNode(intValue).asString(), is(nullValue())); - } - - @Test - public void shouldGetArrayValueOfCustomClass() throws Exception { - JsonNode value = mapper.valueToTree(new UserPojo[]{new UserPojo("George", 1), new UserPojo("Mark", 2)}); - Claim claim = claimFromNode(value); - - assertThat(claim.asArray(UserPojo.class), is(notNullValue())); - assertThat(claim.asArray(UserPojo.class), is(arrayContaining(new UserPojo("George", 1), new UserPojo("Mark", 2)))); - } - - @Test - public void shouldGetArrayValue() throws Exception { - JsonNode value = mapper.valueToTree(new String[]{"string1", "string2"}); - Claim claim = claimFromNode(value); - - assertThat(claim.asArray(String.class), is(notNullValue())); - assertThat(claim.asArray(String.class), is(arrayContaining("string1", "string2"))); - } - - @Test - public void shouldGetNullArrayIfNullValue() throws Exception { - JsonNode value = mapper.valueToTree(null); - Claim claim = claimFromNode(value); - - assertThat(claim.asArray(String.class), is(nullValue())); - } - - @Test - public void shouldGetNullArrayIfNonArrayValue() throws Exception { - JsonNode value = mapper.valueToTree(1); - Claim claim = claimFromNode(value); - - assertThat(claim.asArray(String.class), is(nullValue())); - } - - @Test - public void shouldThrowIfArrayClassMismatch() throws Exception { - JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); - Claim claim = claimFromNode(value); - - exception.expect(JWTDecodeException.class); - claim.asArray(UserPojo.class); - } - - @Test - public void shouldGetListValueOfCustomClass() throws Exception { - JsonNode value = mapper.valueToTree(Arrays.asList(new UserPojo("George", 1), new UserPojo("Mark", 2))); - Claim claim = claimFromNode(value); - - assertThat(claim.asList(UserPojo.class), is(notNullValue())); - assertThat(claim.asList(UserPojo.class), is(hasItems(new UserPojo("George", 1), new UserPojo("Mark", 2)))); - } - - @Test - public void shouldGetListValue() throws Exception { - JsonNode value = mapper.valueToTree(Arrays.asList("string1", "string2")); - Claim claim = claimFromNode(value); - - assertThat(claim.asList(String.class), is(notNullValue())); - assertThat(claim.asList(String.class), is(hasItems("string1", "string2"))); - } - - @Test - public void shouldGetNullListIfNullValue() throws Exception { - JsonNode value = mapper.valueToTree(null); - Claim claim = claimFromNode(value); - - assertThat(claim.asList(String.class), is(nullValue())); - } - - @Test - public void shouldGetNullListIfNonArrayValue() throws Exception { - JsonNode value = mapper.valueToTree(1); - Claim claim = claimFromNode(value); - - assertThat(claim.asList(String.class), is(nullValue())); - } - - @Test - public void shouldThrowIfListClassMismatch() throws Exception { - JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); - Claim claim = claimFromNode(value); - - exception.expect(JWTDecodeException.class); - claim.asList(UserPojo.class); - } - - @Test - public void shouldReturnBaseClaimWhenParsingMissingNode() throws Exception { - JsonNode value = MissingNode.getInstance(); - Claim claim = claimFromNode(value); - - assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); - assertThat(claim.isNull(), is(true)); - } - - @Test - public void shouldReturnBaseClaimWhenParsingNullNode() throws Exception { - JsonNode value = NullNode.getInstance(); - Claim claim = claimFromNode(value); - - assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); - assertThat(claim.isNull(), is(true)); - } - - @Test - public void shouldReturnBaseClaimWhenParsingNullValue() throws Exception { - JsonNode value = mapper.valueToTree(null); - Claim claim = claimFromNode(value); - - assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(NullClaim.class))); - assertThat(claim.isNull(), is(true)); - } - - @Test - public void shouldReturnValidButNullClaimIfTreeIsEmpty() throws Exception { - JsonNode value = mapper.valueToTree(new Object()); - Claim claim = claimFromNode(value); - - assertThat(claim, is(notNullValue())); - assertThat(claim, is(instanceOf(JsonNodeClaim.class))); - assertThat(claim.isNull(), is(true)); - } -} \ 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 09ab42fc..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,19 +5,20 @@ 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; import org.junit.Test; import org.junit.rules.ExpectedException; -import java.io.IOException; - import static com.auth0.jwt.impl.JWTParser.getDefaultObjectMapper; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class JWTParserTest { @@ -26,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))); @@ -39,36 +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 shouldThrowOnReadValueException() throws Exception { - String jsonPayload = "{}"; - exception.expect(JWTDecodeException.class); - exception.expectMessage(String.format("The string '%s' doesn't have a valid JSON format.", jsonPayload)); - - ObjectMapper mapper = mock(ObjectMapper.class); - when(mapper.readValue(eq(jsonPayload), eq(Object.class))).thenThrow(IOException.class); - JWTParser parser = new JWTParser(mapper); - - parser.convertFromJSON(jsonPayload, Object.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)); @@ -79,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)); @@ -95,27 +87,30 @@ public void shouldThrowOnInvalidHeader() throws Exception { } @Test - public void shouldConvertFromValidJSON() throws Exception { - String json = "{}"; - 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 new file mode 100644 index 00000000..a0364953 --- /dev/null +++ b/lib/src/test/java/com/auth0/jwt/impl/JsonNodeClaimTest.java @@ -0,0 +1,533 @@ +package com.auth0.jwt.impl; + +import com.auth0.jwt.UserPojo; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.Claim; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.MissingNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.hamcrest.collection.IsMapContaining; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentMatchers; + +import java.io.IOException; +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 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.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() { + mapper = getDefaultObjectMapper(); + } + + @Test + public void shouldGetBooleanValue() { + JsonNode value = mapper.valueToTree(true); + Claim claim = claimFromNode(value); + + assertThat(claim.asBoolean(), is(notNullValue())); + assertThat(claim.asBoolean(), is(true)); + } + + private Claim claimFromNode(JsonNode value) { + return JsonNodeClaim.claimFromNode(value, mapper); + } + + @Test + public void shouldGetNullBooleanIfNotBooleanValue() { + JsonNode objectValue = mapper.valueToTree(new Object()); + assertThat(claimFromNode(objectValue).asBoolean(), is(nullValue())); + JsonNode stringValue = mapper.valueToTree("boolean"); + assertThat(claimFromNode(stringValue).asBoolean(), is(nullValue())); + } + + @Test + public void shouldGetIntValue() { + JsonNode value = mapper.valueToTree(123); + Claim claim = claimFromNode(value); + + assertThat(claim.asInt(), is(notNullValue())); + assertThat(claim.asInt(), is(123)); + } + + @Test + public void shouldGetNullIntIfNotIntValue() { + JsonNode objectValue = mapper.valueToTree(new Object()); + assertThat(claimFromNode(objectValue).asInt(), is(nullValue())); + JsonNode stringValue = mapper.valueToTree("123"); + assertThat(claimFromNode(stringValue).asInt(), is(nullValue())); + } + + @Test + public void shouldGetLongValue() { + JsonNode value = mapper.valueToTree(Long.MAX_VALUE); + Claim claim = claimFromNode(value); + + assertThat(claim.asLong(), is(notNullValue())); + assertThat(claim.asLong(), is(Long.MAX_VALUE)); + } + + @Test + public void shouldGetNullLongIfNotIntValue() { + JsonNode objectValue = mapper.valueToTree(new Object()); + assertThat(claimFromNode(objectValue).asLong(), is(nullValue())); + JsonNode stringValue = mapper.valueToTree("" + Long.MAX_VALUE); + assertThat(claimFromNode(stringValue).asLong(), is(nullValue())); + } + + @Test + public void shouldGetDoubleValue() { + JsonNode value = mapper.valueToTree(1.5); + Claim claim = claimFromNode(value); + + assertThat(claim.asDouble(), is(notNullValue())); + assertThat(claim.asDouble(), is(1.5)); + } + + @Test + public void shouldGetNullDoubleIfNotDoubleValue() { + JsonNode objectValue = mapper.valueToTree(new Object()); + assertThat(claimFromNode(objectValue).asDouble(), is(nullValue())); + JsonNode stringValue = mapper.valueToTree("123.23"); + assertThat(claimFromNode(stringValue).asDouble(), is(nullValue())); + } + + @Test + public void shouldGetNumericDateValue() { + long seconds = 1476824844L; + JsonNode value = mapper.valueToTree(seconds); + Claim claim = claimFromNode(value); + + assertThat(claim.asDate(), is(new Date(seconds * 1000))); + assertThat(claim.asInstant(), is(Instant.ofEpochSecond(seconds))); + } + + @Test + 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() { + JsonNode value = mapper.valueToTree("string"); + Claim claim = claimFromNode(value); + + assertThat(claim.asString(), is(notNullValue())); + assertThat(claim.asString(), is("string")); + } + + @Test + public void shouldGetNullStringIfNotStringValue() { + JsonNode objectValue = mapper.valueToTree(new Object()); + assertThat(claimFromNode(objectValue).asString(), is(nullValue())); + JsonNode intValue = mapper.valueToTree(12345); + assertThat(claimFromNode(intValue).asString(), is(nullValue())); + } + + @Test + public void shouldGetArrayValueOfCustomClass() { + JsonNode value = mapper.valueToTree(new UserPojo[]{new UserPojo("George", 1), new UserPojo("Mark", 2)}); + Claim claim = claimFromNode(value); + + assertThat(claim.asArray(UserPojo.class), is(notNullValue())); + assertThat(claim.asArray(UserPojo.class), is(arrayContaining(new UserPojo("George", 1), new UserPojo("Mark", 2)))); + } + + @Test + public void shouldGetArrayValue() { + JsonNode value = mapper.valueToTree(new String[]{"string1", "string2"}); + Claim claim = claimFromNode(value); + + assertThat(claim.asArray(String.class), is(notNullValue())); + assertThat(claim.asArray(String.class), is(arrayContaining("string1", "string2"))); + } + + @Test + public void shouldGetNullArrayIfNullValue() { + JsonNode value = mapper.valueToTree(null); + Claim claim = claimFromNode(value); + + assertThat(claim.asArray(String.class), is(nullValue())); + } + + @Test + public void shouldGetNullArrayIfNonArrayValue() { + JsonNode value = mapper.valueToTree(1); + Claim claim = claimFromNode(value); + + assertThat(claim.asArray(String.class), is(nullValue())); + } + + @Test + public void shouldThrowIfArrayClassMismatch() { + JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); + Claim claim = claimFromNode(value); + + exception.expect(JWTDecodeException.class); + claim.asArray(UserPojo.class); + } + + @Test + public void shouldGetListValueOfCustomClass() { + JsonNode value = mapper.valueToTree(Arrays.asList(new UserPojo("George", 1), new UserPojo("Mark", 2))); + Claim claim = claimFromNode(value); + + assertThat(claim.asList(UserPojo.class), is(notNullValue())); + assertThat(claim.asList(UserPojo.class), is(hasItems(new UserPojo("George", 1), new UserPojo("Mark", 2)))); + } + + @Test + public void shouldGetListValue() { + JsonNode value = mapper.valueToTree(Arrays.asList("string1", "string2")); + Claim claim = claimFromNode(value); + + assertThat(claim.asList(String.class), is(notNullValue())); + assertThat(claim.asList(String.class), is(hasItems("string1", "string2"))); + } + + @Test + public void shouldGetNullListIfNullValue() { + JsonNode value = mapper.valueToTree(null); + Claim claim = claimFromNode(value); + + assertThat(claim.asList(String.class), is(nullValue())); + } + + @Test + public void shouldGetNullListIfNonArrayValue() { + JsonNode value = mapper.valueToTree(1); + Claim claim = claimFromNode(value); + + assertThat(claim.asList(String.class), is(nullValue())); + } + + @Test + public void shouldThrowIfListClassMismatch() { + JsonNode value = mapper.valueToTree(new String[]{"keys", "values"}); + Claim claim = claimFromNode(value); + + exception.expect(JWTDecodeException.class); + claim.asList(UserPojo.class); + } + + @Test + public void shouldGetNullMapIfNullValue() { + JsonNode value = mapper.valueToTree(null); + Claim claim = claimFromNode(value); + + assertThat(claim.asMap(), is(nullValue())); + } + + @Test + public void shouldGetNullMapIfNonArrayValue() { + JsonNode value = mapper.valueToTree(1); + Claim claim = claimFromNode(value); + + assertThat(claim.asMap(), is(nullValue())); + } + + @Test + public void shouldGetMapValue() { + Map map = new HashMap<>(); + map.put("text", "extraValue"); + map.put("number", 12); + map.put("boolean", true); + map.put("object", Collections.singletonMap("something", "else")); + + JsonNode value = mapper.valueToTree(map); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + Map backMap = claim.asMap(); + assertThat(backMap, is(notNullValue())); + assertThat(backMap, hasEntry("text", (Object) "extraValue")); + assertThat(backMap, hasEntry("number", (Object) 12)); + assertThat(backMap, hasEntry("boolean", (Object) true)); + assertThat(backMap, hasKey("object")); + assertThat((Map) backMap.get("object"), IsMapContaining.hasEntry("something", (Object) "else")); + } + + @Test + public void shouldThrowIfAnExtraordinaryExceptionHappensWhenParsingAsGenericMap() throws Exception { + JsonNode value = mock(ObjectNode.class); + when(value.getNodeType()).thenReturn(JsonNodeType.OBJECT); + + ObjectMapper mockedMapper = mock(ObjectMapper.class); + + 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); + + exception.expect(JWTDecodeException.class); + spiedClaim.asMap(); + } + + @Test + public void shouldGetCustomClassValue() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + assertThat(claim.as(UserPojo.class).getName(), is("john")); + assertThat(claim.as(UserPojo.class).getId(), is(123)); + } + + @Test + public void shouldThrowIfCustomClassMismatch() { + JsonNode value = mapper.valueToTree(new UserPojo("john", 123)); + Claim claim = claimFromNode(value); + + exception.expect(JWTDecodeException.class); + 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() { + JsonNode value = mapper.valueToTree(Collections.singletonMap("key", new UserPojo("john", 123))); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + Map map = claim.as(Map.class); + assertThat(((Map) map.get("key")), hasEntry("name", (Object) "john")); + assertThat(((Map) map.get("key")), hasEntry("id", (Object) 123)); + } + + @Test + public void shouldReturnBaseClaimWhenParsingMissingNode() { + JsonNode value = MissingNode.getInstance(); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + assertThat(claim.isMissing(), is(true)); + assertThat(claim.isNull(), is(false)); + } + + @Test + public void shouldReturnBaseClaimWhenParsingNullNode() { + JsonNode value = NullNode.getInstance(); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(false)); + } + + @Test + public void shouldReturnBaseClaimWhenParsingNullValue() { + JsonNode value = mapper.valueToTree(null); + Claim claim = claimFromNode(value); + + assertThat(claim, is(notNullValue())); + assertThat(claim.isNull(), is(true)); + assertThat(claim.isMissing(), is(false)); + } + + @Test + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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() { + 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/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 faf9dd4e..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,108 +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 shouldGetLargeInstantWhenParsingNumericNode() { + Map tree = new HashMap<>(); + long seconds = Integer.MAX_VALUE + 10000L; + LongNode node = new LongNode(seconds); + tree.put("key", node); + + 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); @@ -230,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); @@ -239,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 6563f8e8..5ad7ac68 100644 --- a/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java +++ b/lib/src/test/java/com/auth0/jwt/impl/PayloadImplTest.java @@ -1,20 +1,21 @@ package com.auth0.jwt.impl; +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.*; @@ -24,133 +25,170 @@ 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() { + 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, objectMapper); + assertThat(payload, is(notNullValue())); + Map claims = payload.getClaims(); + assertThat(claims, is(notNullValue())); + + assertThat(claims.get("extraClaim"), is(notNullValue())); + assertThat(claims.get("sub"), is(notNullValue())); + } + + @Test + public void shouldNotAllowToModifyClaimsMap() { + assertThat(payload, is(notNullValue())); + Map claims = payload.getClaims(); + assertThat(claims, is(notNullValue())); + exception.expect(UnsupportedOperationException.class); + claims.put("name", null); } } \ No newline at end of file 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 e49e1e53..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,14 +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.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; public class PayloadSerializerTest { @@ -35,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(); @@ -47,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(); @@ -56,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(); @@ -65,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(); @@ -74,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(); @@ -83,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(); @@ -92,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(); @@ -101,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(); @@ -110,16 +159,53 @@ 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(); assertThat(writer.toString(), is(equalTo("{\"birthdate\":1478874}"))); } + @Test + public void shouldSerializeDatesUsingLong() throws Exception { + long secs = Integer.MAX_VALUE + 10000L; + Date date = new Date(secs * 1000L); + Map claims = new HashMap<>(); + claims.put("iat", date); + claims.put("nbf", date); + claims.put("exp", date); + claims.put("ctm", date); + 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(); @@ -128,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(); @@ -137,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(); @@ -146,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(); @@ -155,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(); @@ -166,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(); @@ -177,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(); @@ -187,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 8c8ef3c7..00000000 --- a/scripts/release.gradle +++ /dev/null @@ -1,167 +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 updated = "compile '${project.group}:${project.name}:${next}'" - def oldSingleQuote = "compile '${project.group}:${project.name}:${current}'" - def oldDoubleQuote = "compile \"${project.group}:${project.name}:${current}\"" - def contents = file.getText('UTF-8') - contents = contents.replace(oldSingleQuote, updated).replace(oldDoubleQuote, updated) - 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')