├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── actions │ ├── get-prerelease │ │ └── action.yml │ ├── get-release-notes │ │ └── action.yml │ ├── get-version │ │ └── action.yml │ ├── maven-publish │ │ └── action.yml │ ├── release-create │ │ └── action.yml │ ├── rl-scanner │ │ └── action.yml │ └── tag-exists │ │ └── action.yml ├── stale.yml └── workflows │ ├── build-and-test.yml │ ├── dependabot.yml │ ├── gradle-wrapper-validation.yml │ ├── java-release.yml │ ├── release.yml │ ├── rl-secure.yml │ ├── semgrep.yml │ └── snyk.yml ├── .gitignore ├── .shiprc ├── .version ├── CHANGELOG.md ├── EXAMPLES.md ├── LICENSE ├── README.md ├── build.gradle ├── codecov.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── opslevel.yml ├── settings.gradle └── src ├── main └── java │ └── com │ └── auth0 │ └── jwk │ ├── Bucket.java │ ├── BucketImpl.java │ ├── GuavaCachedJwkProvider.java │ ├── InvalidPublicKeyException.java │ ├── Jwk.java │ ├── JwkException.java │ ├── JwkProvider.java │ ├── JwkProviderBuilder.java │ ├── NetworkException.java │ ├── RateLimitReachedException.java │ ├── RateLimitedJwkProvider.java │ ├── SigningKeyNotFoundException.java │ ├── UrlJwkProvider.java │ ├── Util.java │ └── VisibleForTesting.java └── test ├── java └── com │ └── auth0 │ └── jwk │ ├── BucketImplTest.java │ ├── GuavaCachedJwkProviderTest.java │ ├── JwkProviderBuilderTest.java │ ├── JwkTest.java │ ├── RateLimitReachedExceptionTest.java │ ├── RateLimitedJwkProviderTest.java │ ├── UrlJwkProviderTest.java │ └── UtilTest.java └── resources ├── empty-jwks.json ├── invalid-jwks.json ├── jwks-single-no-kid.json ├── jwks-single.json └── jwks.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0/project-dx-sdks-engineer-codeowner 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: Have you found a bug or issue? Create a bug report for this library 3 | labels: ["bug"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 10 | 11 | - type: checkboxes 12 | id: checklist 13 | attributes: 14 | label: Checklist 15 | options: 16 | - label: I have looked into the [Readme](https://github.com/auth0/jwks-rsa-java#readme) and [Examples](https://github.com/auth0/jwks-rsa-java/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. 17 | required: true 18 | - label: I have looked into the [API documentation](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/index.html) and have not found a suitable solution or answer. 19 | required: true 20 | - label: I have searched the [issues](https://github.com/auth0/jwks-rsa-java/issues) and have not found a suitable solution or answer. 21 | required: true 22 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 23 | required: true 24 | - 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). 25 | required: true 26 | 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: Description 31 | description: Provide a clear and concise description of the issue, including what you expected to happen. 32 | validations: 33 | required: true 34 | 35 | - type: textarea 36 | id: reproduction 37 | attributes: 38 | label: Reproduction 39 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 40 | placeholder: | 41 | 1. Step 1... 42 | 2. Step 2... 43 | 3. ... 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | id: additional-context 49 | attributes: 50 | label: Additional context 51 | description: Other libraries that might be involved, or any other relevant information you think would be useful. 52 | validations: 53 | required: false 54 | 55 | - type: input 56 | id: environment-version 57 | attributes: 58 | label: jwks-rsa version 59 | validations: 60 | required: true 61 | 62 | - type: input 63 | id: environment-java-version 64 | attributes: 65 | label: Java version 66 | validations: 67 | required: true 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: 🧩 Feature request 2 | description: Suggest an idea or a feature for this library 3 | labels: ["feature request"] 4 | 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have looked into the [Readme](https://github.com/auth0/jwks-rsa-java#readme) and [Examples](https://github.com/auth0/jwks-rsa-java/blob/master/EXAMPLES.md), and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have looked into the [API documentation](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/index.html) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [issues](https://github.com/auth0/jwks-rsa-java/issues) and have not found a suitable solution or answer. 16 | required: true 17 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 18 | required: true 19 | - 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). 20 | required: true 21 | 22 | - type: textarea 23 | id: description 24 | attributes: 25 | label: Describe the problem you'd like to have solved 26 | description: A clear and concise description of what the problem is. 27 | placeholder: I'm always frustrated when... 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: ideal-solution 33 | attributes: 34 | label: Describe the ideal solution 35 | description: A clear and concise description of what you want to happen. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: alternatives-and-workarounds 41 | attributes: 42 | label: Alternatives and current workarounds 43 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 44 | validations: 45 | required: false 46 | 47 | - type: textarea 48 | id: additional-context 49 | attributes: 50 | label: Additional context 51 | description: Add any other context or screenshots about the feature request here. 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Auth0 Community 4 | url: https://community.auth0.com 5 | about: Discuss this library in the Auth0 Community forums 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Changes 2 | 3 | Please describe both what is changing and why this is important. Include: 4 | 5 | - Endpoints added, deleted, deprecated, or changed 6 | - Classes and methods added, deleted, deprecated, or changed 7 | - Screenshots of new or changed UI, if applicable 8 | - 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) 9 | - Any alternative designs or approaches considered 10 | 11 | ### References 12 | 13 | Please include relevant links supporting this change such as a: 14 | 15 | - support ticket 16 | - community post 17 | - StackOverflow post 18 | - support forum thread 19 | 20 | ### Testing 21 | 22 | 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. 23 | 24 | - [ ] This change adds test coverage 25 | - [ ] This change has been tested on the latest version of Java or why not 26 | 27 | ### Checklist 28 | 29 | - [ ] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 30 | - [ ] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) 31 | - [ ] All existing and new tests complete without errors 32 | -------------------------------------------------------------------------------- /.github/actions/get-prerelease/action.yml: -------------------------------------------------------------------------------- 1 | name: Return a boolean indicating if the version contains prerelease identifiers 2 | 3 | # 4 | # Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | version: 11 | required: true 12 | 13 | outputs: 14 | prerelease: 15 | value: ${{ steps.get_prerelease.outputs.PRERELEASE }} 16 | 17 | runs: 18 | using: composite 19 | 20 | steps: 21 | - id: get_prerelease 22 | shell: bash 23 | run: | 24 | if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then 25 | echo "PRERELEASE=true" >> $GITHUB_OUTPUT 26 | else 27 | echo "PRERELEASE=false" >> $GITHUB_OUTPUT 28 | fi 29 | env: 30 | VERSION: ${{ inputs.version }} 31 | -------------------------------------------------------------------------------- /.github/actions/get-release-notes/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the release notes extracted from the body of the PR associated with the release. 2 | 3 | # 4 | # 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. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | inputs: 9 | version: 10 | required: true 11 | repo_name: 12 | required: false 13 | repo_owner: 14 | required: true 15 | token: 16 | required: true 17 | 18 | outputs: 19 | release-notes: 20 | value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} 21 | 22 | runs: 23 | using: composite 24 | 25 | steps: 26 | - uses: actions/github-script@v7 27 | id: get_release_notes 28 | with: 29 | result-encoding: string 30 | script: | 31 | const { data: pulls } = await github.rest.pulls.list({ 32 | owner: process.env.REPO_OWNER, 33 | repo: process.env.REPO_NAME, 34 | state: 'all', 35 | head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, 36 | }); 37 | core.setOutput('RELEASE_NOTES', pulls[0].body); 38 | env: 39 | GITHUB_TOKEN: ${{ inputs.token }} 40 | REPO_OWNER: ${{ inputs.repo_owner }} 41 | REPO_NAME: ${{ inputs.repo_name }} 42 | VERSION: ${{ inputs.version }} 43 | -------------------------------------------------------------------------------- /.github/actions/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the version extracted from the branch name 2 | 3 | # 4 | # Returns the version from the .version file. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | outputs: 10 | version: 11 | value: ${{ steps.get_version.outputs.VERSION }} 12 | 13 | runs: 14 | using: composite 15 | 16 | steps: 17 | - id: get_version 18 | shell: bash 19 | run: | 20 | VERSION=$(head -1 .version) 21 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 22 | -------------------------------------------------------------------------------- /.github/actions/maven-publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish release to Java 2 | 3 | inputs: 4 | ossr-username: 5 | required: true 6 | ossr-password: 7 | required: true 8 | signing-key: 9 | required: true 10 | signing-password: 11 | required: true 12 | java-version: 13 | required: true 14 | is-android: 15 | required: true 16 | version: 17 | required: true 18 | 19 | runs: 20 | using: composite 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Java 27 | shell: bash 28 | run: | 29 | curl -s "https://get.sdkman.io" | bash 30 | source "/home/runner/.sdkman/bin/sdkman-init.sh" 31 | sdk list java 32 | sdk install java ${{ inputs.java-version }} && sdk default java ${{ inputs.java-version }} 33 | 34 | - uses: gradle/wrapper-validation-action@56b90f209b02bf6d1deae490e9ef18b21a389cd4 # pin@1.1.0 35 | 36 | - name: Publish Java 37 | shell: bash 38 | if: inputs.is-android == 'false' 39 | run: ./gradlew clean assemble sign publishMavenJavaPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" 40 | 41 | - name: Publish Android 42 | shell: bash 43 | if: inputs.is-android == 'true' 44 | run: ./gradlew clean assemble sign publishAndroidLibraryPublicationToMavenRepository -PisSnapshot=false -Pversion="${{ inputs.version }}" -PossrhUsername="${{ inputs.ossr-username }}" -PossrhPassword="${{ inputs.ossr-password }}" -PsigningKey="${{ inputs.signing-key }}" -PsigningPassword="${{ inputs.signing-password }}" 45 | -------------------------------------------------------------------------------- /.github/actions/release-create/action.yml: -------------------------------------------------------------------------------- 1 | name: Create a GitHub release 2 | 3 | # 4 | # Creates a GitHub release with the given version. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | token: 11 | required: true 12 | files: 13 | required: false 14 | name: 15 | required: true 16 | body: 17 | required: true 18 | tag: 19 | required: true 20 | commit: 21 | required: true 22 | draft: 23 | default: false 24 | required: false 25 | prerelease: 26 | default: false 27 | required: false 28 | fail_on_unmatched_files: 29 | default: true 30 | required: false 31 | 32 | runs: 33 | using: composite 34 | 35 | steps: 36 | - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 37 | with: 38 | body: ${{ inputs.body }} 39 | name: ${{ inputs.name }} 40 | tag_name: ${{ inputs.tag }} 41 | target_commitish: ${{ inputs.commit }} 42 | draft: ${{ inputs.draft }} 43 | prerelease: ${{ inputs.prerelease }} 44 | fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} 45 | files: ${{ inputs.files }} 46 | env: 47 | GITHUB_TOKEN: ${{ inputs.token }} 48 | -------------------------------------------------------------------------------- /.github/actions/rl-scanner/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Reversing Labs Scanner' 2 | description: 'Runs the Reversing Labs scanner on a specified artifact.' 3 | inputs: 4 | artifact-path: 5 | description: 'Path to the artifact to be scanned.' 6 | required: true 7 | version: 8 | description: 'Version of the artifact.' 9 | required: true 10 | 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.10' 18 | 19 | - name: Install Python dependencies 20 | shell: bash 21 | run: | 22 | pip install boto3 requests 23 | 24 | - name: Configure AWS credentials 25 | uses: aws-actions/configure-aws-credentials@v1 26 | with: 27 | role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} 28 | aws-region: us-east-1 29 | mask-aws-account-id: true 30 | 31 | - name: Install RL Wrapper 32 | shell: bash 33 | run: | 34 | 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" 35 | 36 | - name: Run RL Scanner 37 | shell: bash 38 | env: 39 | RLSECURE_LICENSE: ${{ env.RLSECURE_LICENSE }} 40 | RLSECURE_SITE_KEY: ${{ env.RLSECURE_SITE_KEY }} 41 | SIGNAL_HANDLER_TOKEN: ${{ env.SIGNAL_HANDLER_TOKEN }} 42 | PYTHONUNBUFFERED: 1 43 | run: | 44 | if [ ! -f "${{ inputs.artifact-path }}" ]; then 45 | echo "Artifact not found: ${{ inputs.artifact-path }}" 46 | exit 1 47 | fi 48 | 49 | rl-wrapper \ 50 | --artifact "${{ inputs.artifact-path }}" \ 51 | --name "${{ github.event.repository.name }}" \ 52 | --version "${{ inputs.version }}" \ 53 | --repository "${{ github.repository }}" \ 54 | --commit "${{ github.sha }}" \ 55 | --build-env "github_actions" \ 56 | --suppress_output 57 | 58 | # Check the outcome of the scanner 59 | if [ $? -ne 0 ]; then 60 | echo "RL Scanner failed." 61 | echo "scan-status=failed" >> $GITHUB_ENV 62 | exit 1 63 | else 64 | echo "RL Scanner passed." 65 | echo "scan-status=success" >> $GITHUB_ENV 66 | fi 67 | 68 | outputs: 69 | scan-status: 70 | description: 'The outcome of the scan process.' 71 | value: ${{ env.scan-status }} 72 | -------------------------------------------------------------------------------- /.github/actions/tag-exists/action.yml: -------------------------------------------------------------------------------- 1 | name: Return a boolean indicating if a tag already exists for the repository 2 | 3 | # 4 | # Returns a simple true/false boolean indicating whether the tag exists or not. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | token: 11 | required: true 12 | tag: 13 | required: true 14 | 15 | outputs: 16 | exists: 17 | description: 'Whether the tag exists or not' 18 | value: ${{ steps.tag-exists.outputs.EXISTS }} 19 | 20 | runs: 21 | using: composite 22 | 23 | steps: 24 | - id: tag-exists 25 | shell: bash 26 | run: | 27 | GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" 28 | http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") 29 | if [ "$http_status_code" -ne "404" ] ; then 30 | echo "EXISTS=true" >> $GITHUB_OUTPUT 31 | else 32 | echo "EXISTS=false" >> $GITHUB_OUTPUT 33 | fi 34 | env: 35 | TAG_NAME: ${{ inputs.tag }} 36 | GITHUB_TOKEN: ${{ inputs.token }} 37 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | daysUntilClose: 7 8 | 9 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 10 | exemptLabels: [] 11 | 12 | # Set to true to ignore issues with an assignee (defaults to false) 13 | exemptAssignees: true 14 | 15 | # Label to use when marking as stale 16 | staleLabel: closed:stale 17 | 18 | # Comment to post when marking as stale. Set to `false` to disable 19 | markComment: > 20 | 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! 🙇‍♂️ -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: auth0/jwks-rsa-java/build-and-test 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: ["master", "main"] 8 | 9 | jobs: 10 | gradle: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: temurin 17 | java-version: 8 18 | - uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c 19 | with: 20 | arguments: assemble apiDiff check jacocoTestReport --continue --console=plain 21 | - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d 22 | with: 23 | flags: unittests 24 | - uses: actions/upload-artifact@v3 25 | with: 26 | name: Reports 27 | path: lib/build/reports 28 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "gradle" 9 | directory: "lib" 10 | schedule: 11 | interval: "daily" 12 | ignore: 13 | - dependency-name: "*" 14 | update-types: ["version-update:semver-major"] 15 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | validation: 6 | name: "validation/gradlew" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: gradle/wrapper-validation-action@8d49e559aae34d3e0eb16cde532684bc9702762b # pin@1.0.6 11 | -------------------------------------------------------------------------------- /.github/workflows/java-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Java and GitHub Release 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | java-version: 7 | required: true 8 | type: string 9 | is-android: 10 | required: true 11 | type: string 12 | secrets: 13 | ossr-username: 14 | required: true 15 | ossr-password: 16 | required: true 17 | signing-key: 18 | required: true 19 | signing-password: 20 | required: true 21 | github-token: 22 | required: true 23 | 24 | ### TODO: Replace instances of './.github/actions/' w/ `auth0/dx-sdk-actions/` and append `@latest` after the common `dx-sdk-actions` repo is made public. 25 | ### 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. 26 | 27 | jobs: 28 | release: 29 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 30 | runs-on: ubuntu-latest 31 | environment: release 32 | 33 | steps: 34 | # Checkout the code 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | 39 | # Get the version from the branch name 40 | - id: get_version 41 | uses: ./.github/actions/get-version 42 | 43 | # Get the prerelease flag from the branch name 44 | - id: get_prerelease 45 | uses: ./.github/actions/get-prerelease 46 | with: 47 | version: ${{ steps.get_version.outputs.version }} 48 | 49 | # Get the release notes 50 | - id: get_release_notes 51 | uses: ./.github/actions/get-release-notes 52 | with: 53 | token: ${{ secrets.github-token }} 54 | version: ${{ steps.get_version.outputs.version }} 55 | repo_owner: ${{ github.repository_owner }} 56 | repo_name: ${{ github.event.repository.name }} 57 | 58 | # Check if the tag already exists 59 | - id: tag_exists 60 | uses: ./.github/actions/tag-exists 61 | with: 62 | tag: ${{ steps.get_version.outputs.version }} 63 | token: ${{ secrets.github-token }} 64 | 65 | # If the tag already exists, exit with an error 66 | - if: steps.tag_exists.outputs.exists == 'true' 67 | run: exit 1 68 | 69 | # Publish the release to Maven 70 | - uses: ./.github/actions/maven-publish 71 | with: 72 | java-version: ${{ inputs.java-version }} 73 | is-android: ${{ inputs.is-android }} 74 | version: ${{ steps.get_version.outputs.version }} 75 | ossr-username: ${{ secrets.ossr-username }} 76 | ossr-password: ${{ secrets.ossr-password }} 77 | signing-key: ${{ secrets.signing-key }} 78 | signing-password: ${{ secrets.signing-password }} 79 | 80 | # Create a release for the tag 81 | - uses: ./.github/actions/release-create 82 | with: 83 | token: ${{ secrets.github-token }} 84 | name: ${{ steps.get_version.outputs.version }} 85 | body: ${{ steps.get_release_notes.outputs.release-notes }} 86 | tag: ${{ steps.get_version.outputs.version }} 87 | commit: ${{ github.sha }} 88 | prerelease: ${{ steps.get_prerelease.outputs.prerelease }} 89 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create GitHub Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | id-token: write # This is required for requesting the JWT 12 | 13 | 14 | ### 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. 15 | ### 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. 16 | ### TODO: Also remove `java-release` workflow from this repo's .github/workflows folder once the repo is public. 17 | 18 | jobs: 19 | 20 | rl-scanner: 21 | uses: ./.github/workflows/rl-secure.yml 22 | with: 23 | java-version: 8 24 | artifact-name: "jwks-rsa-java.tgz" 25 | secrets: 26 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 27 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 28 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 29 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 30 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 31 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 32 | 33 | release: 34 | uses: ./.github/workflows/java-release.yml 35 | needs: rl-scanner 36 | with: 37 | java-version: 8.0.382-tem 38 | is-android: false 39 | secrets: 40 | ossr-username: ${{ secrets.OSSR_USERNAME }} 41 | ossr-password: ${{ secrets.OSSR_PASSWORD }} 42 | signing-key: ${{ secrets.SIGNING_KEY }} 43 | signing-password: ${{ secrets.SIGNING_PASSWORD }} 44 | github-token: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.github/workflows/rl-secure.yml: -------------------------------------------------------------------------------- 1 | name: RL-Secure Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | java-version: 7 | required: true 8 | type: string 9 | artifact-name: 10 | required: true 11 | type: string 12 | secrets: 13 | RLSECURE_LICENSE: 14 | required: true 15 | RLSECURE_SITE_KEY: 16 | required: true 17 | SIGNAL_HANDLER_TOKEN: 18 | required: true 19 | PRODSEC_TOOLS_USER: 20 | required: true 21 | PRODSEC_TOOLS_TOKEN: 22 | required: true 23 | PRODSEC_TOOLS_ARN: 24 | required: true 25 | 26 | jobs: 27 | checkout-build-scan-only: 28 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 29 | runs-on: ubuntu-latest 30 | outputs: 31 | scan-status: ${{ steps.rl-scan-conclusion.outcome }} 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v3 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Set up Java 40 | uses: actions/setup-java@v4 41 | with: 42 | distribution: temurin 43 | java-version: ${{ inputs.java-version }} 44 | 45 | - name: Build with Gradle 46 | uses: gradle/gradle-build-action@a4cf152f482c7ca97ef56ead29bf08bcd953284c 47 | with: 48 | arguments: assemble apiDiff check jacocoTestReport --continue --console=plain 49 | 50 | - name: Get Artifact Version 51 | id: get_version 52 | uses: ./.github/actions/get-version 53 | 54 | - name: Create tgz build artifact 55 | run: | 56 | tar -czvf ${{ inputs.artifact-name }} * 57 | 58 | - name: Run RL Scanner 59 | id: rl-scan-conclusion 60 | uses: ./.github/actions/rl-scanner 61 | with: 62 | artifact-path: "$(pwd)/${{ inputs.artifact-name }}" 63 | version: "${{ steps.get_version.outputs.version }}" 64 | env: 65 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 66 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 67 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 68 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 69 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 70 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 71 | 72 | - name: Output scan result 73 | run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request: {} 5 | 6 | push: 7 | branches: ["master", "main"] 8 | 9 | schedule: 10 | - cron: '30 0 1,15 * *' 11 | 12 | jobs: 13 | semgrep: 14 | name: Scan 15 | runs-on: ubuntu-latest 16 | container: 17 | image: returntocorp/semgrep 18 | if: (github.actor != 'dependabot[bot]') 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - run: semgrep ci 23 | env: 24 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Snyk 2 | 3 | on: 4 | merge_group: 5 | workflow_dispatch: 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | push: 11 | branches: 12 | - master 13 | schedule: 14 | - cron: '30 0 1,15 * *' 15 | 16 | permissions: 17 | contents: read 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 21 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 22 | 23 | jobs: 24 | 25 | check: 26 | name: Check for Vulnerabilities 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 31 | 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. 32 | 33 | - uses: actions/checkout@v4 34 | with: 35 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 36 | 37 | - uses: snyk/actions/gradle-jdk11@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0 38 | env: 39 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target/ 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | 15 | # macOS 16 | .DS_Store 17 | 18 | # IntelliJ 19 | .idea 20 | *.iml 21 | out/ 22 | 23 | ### Gradle ### 24 | .gradle 25 | **/build/ 26 | 27 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 28 | !gradle-wrapper.jar -------------------------------------------------------------------------------- /.shiprc: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "README.md": [], 4 | ".version": [], 5 | "build.gradle": ["version = \"{MAJOR}.{MINOR}.{PATCH}\""] 6 | }, 7 | "prefixVersion": false 8 | } -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 0.22.1 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.22.1](https://github.com/auth0/jwks-rsa-java/tree/0.22.1) (2023-07-28) 4 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.22.0...0.22.1) 5 | 6 | **Security** 7 | - Update to Guava 32.1.1 [\#184](https://github.com/auth0/jwks-rsa-java/pull/184) ([jimmyjames](https://github.com/jimmyjames)) 8 | 9 | **Changed** 10 | - Update and pin Gradle workflow actions [\#182](https://github.com/auth0/jwks-rsa-java/pull/182) ([evansims](https://github.com/evansims)) 11 | - Update dependencies [\#175](https://github.com/auth0/jwks-rsa-java/pull/175) ([jimmyjames](https://github.com/jimmyjames)) 12 | - Update JavaDocs with default caching behavior [\#179](https://github.com/auth0/jwks-rsa-java/pull/179) ([msailes](https://github.com/msailes)) 13 | 14 | ## [0.22.0](https://github.com/auth0/jwks-rsa-java/tree/0.22.0) (2023-02-10) 15 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.21.3...0.22.0) 16 | 17 | **Fixed** 18 | - Throw proper exception from cached provider [\#169](https://github.com/auth0/jwks-rsa-java/pull/169) ([jimmyjames](https://github.com/jimmyjames)) 19 | 20 | ## [0.21.3](https://github.com/auth0/jwks-rsa-java/tree/0.21.3) (2023-01-11) 21 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.21.2...0.21.3) 22 | 23 | This patch release uses an updated signing key for verification as part of our commitment to best security practices. 24 | Please review [the README note for additional details.](https://github.com/auth0/jwks-rsa-java/blob/master/README.md) 25 | 26 | **Security** 27 | - Bump `jackson-databind` dependency to 2.13.4.2 [\#162](https://github.com/auth0/jwks-rsa-java/pull/162) ([evansims](https://github.com/evansims)) 28 | 29 | **Changed** 30 | - Update Gradle wrapper to 6.9.2 [\#156](https://github.com/auth0/jwks-rsa-java/pull/156) ([jimmyjames](https://github.com/jimmyjames)) 31 | 32 | ## [0.21.2](https://github.com/auth0/jwks-rsa-java/tree/0.21.2) (2022-09-15) 33 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.21.1...0.21.2) 34 | 35 | **Changed** 36 | - Update Gradle OSS plugin [\#154](https://github.com/auth0/jwks-rsa-java/pull/154) ([jimmyjames](https://github.com/jimmyjames)) 37 | 38 | **Fixed** 39 | - Clarify that getting a JWK is a synchronous operation [\#151](https://github.com/auth0/jwks-rsa-java/pull/151) ([jimmyjames](https://github.com/jimmyjames)) 40 | - [SDK-3623] Fix ECPoint construction for getting public key [\#153](https://github.com/auth0/jwks-rsa-java/pull/153) ([jimmyjames](https://github.com/jimmyjames)) 41 | 42 | ## [0.21.1](https://github.com/auth0/jwks-rsa-java/tree/0.21.1) (2022-03-30) 43 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.21.0...0.21.1) 44 | 45 | **Security** 46 | - Security: Bump `jackson-databind` to 2.13.2.2 [\#144](https://github.com/auth0/jwks-rsa-java/pull/144) ([evansims](https://github.com/evansims)) 47 | 48 | ## [0.21.0](https://github.com/auth0/jwks-rsa-java/tree/0.21.0) (2022-03-14) 49 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.20.2...0.21.0) 50 | 51 | **Security** 52 | - Bump `jackson-databind` dependency to 2.13.2 [\#142](https://github.com/auth0/jwks-rsa-java/pull/142) ([evansims](https://github.com/evansims)) 53 | 54 | ## [0.20.2](https://github.com/auth0/jwks-rsa-java/tree/0.20.2) (2022-02-01) 55 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.20.1...0.20.2) 56 | 57 | **Changed** 58 | - Configure cache with TimeUnit instead of Duration [\#140](https://github.com/auth0/jwks-rsa-java/pull/140) ([jimmyjames](https://github.com/jimmyjames)) 59 | 60 | ## [0.20.1](https://github.com/auth0/jwks-rsa-java/tree/0.20.1) (2022-01-17) 61 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.20.0...0.20.1) 62 | 63 | **Security** 64 | - Update jackson dependency [\#138](https://github.com/auth0/jwks-rsa-java/pull/138) ([poovamraj](https://github.com/poovamraj)) 65 | 66 | ## [0.20.0](https://github.com/auth0/jwks-rsa-java/tree/0.20.0) (2021-10-11) 67 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.19.0...0.20.0) 68 | 69 | **Added** 70 | - Expose read/connect timeouts in the JwkProviderBuilder class [\#133](https://github.com/auth0/jwks-rsa-java/pull/133) ([lbalmaceda](https://github.com/lbalmaceda)) 71 | 72 | **Changed** 73 | - Use Duration in JwkProviderBuilder [\#130](https://github.com/auth0/jwks-rsa-java/pull/130) ([ghostd](https://github.com/ghostd)) 74 | 75 | ## [0.19.0](https://github.com/auth0/jwks-rsa-java/tree/0.19.0) (2021-07-05) 76 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.18.0...0.19.0) 77 | 78 | **Changed** 79 | - Update OSS release plugin version [\#129](https://github.com/auth0/jwks-rsa-java/pull/129) ([lbalmaceda](https://github.com/lbalmaceda)) 80 | 81 | ## [0.18.0](https://github.com/auth0/jwks-rsa-java/tree/0.18.0) (2021-05-10) 82 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.17.1...0.18.0) 83 | 84 | **Changed** 85 | - Remove bintray gradle plugin [\#124](https://github.com/auth0/jwks-rsa-java/pull/124) ([lbalmaceda](https://github.com/lbalmaceda)) 86 | - Remove Guava usage where possible [\#121](https://github.com/auth0/jwks-rsa-java/pull/121) ([XakepSDK](https://github.com/XakepSDK)) 87 | 88 | ## [0.17.1](https://github.com/auth0/jwks-rsa-java/tree/0.17.1) (2021-04-19) 89 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.17.0...0.17.1) 90 | **Closed issues** 91 | - Remove commons-codec dependency [\#118](https://github.com/auth0/jwks-rsa-java/issues/118) 92 | 93 | **Changed** 94 | - Update OSS publishing plugin [\#122](https://github.com/auth0/jwks-rsa-java/pull/122) ([jimmyjames](https://github.com/jimmyjames)) 95 | 96 | ## [0.17.0](https://github.com/auth0/jwks-rsa-java/tree/0.17.0) (2021-02-26) 97 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.16.0...0.17.0) 98 | 99 | **Added** 100 | - Allow setting of custom headers [\#115](https://github.com/auth0/jwks-rsa-java/pull/115) ([jimmyjames](https://github.com/jimmyjames)) 101 | 102 | ## [0.16.0](https://github.com/auth0/jwks-rsa-java/tree/0.16.0) (2021-02-22) 103 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.15.0...0.16.0) 104 | 105 | **Added** 106 | - Support Elliptic Curve public keys [\#110](https://github.com/auth0/jwks-rsa-java/pull/110) ([JesseEstum](https://github.com/JesseEstum)) 107 | 108 | **Fixed** 109 | - toString should return kty instead of kyt [\#108](https://github.com/auth0/jwks-rsa-java/pull/108) ([pevers](https://github.com/pevers)) 110 | 111 | ## [0.15.0](https://github.com/auth0/jwks-rsa-java/tree/0.15.0) (2020-11-16) 112 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.14.1...0.15.0) 113 | 114 | **Changed** 115 | - Target Java 8 [\#105](https://github.com/auth0/jwks-rsa-java/pull/105) ([lbalmaceda](https://github.com/lbalmaceda)) 116 | 117 | ## [0.14.1](https://github.com/auth0/jwks-rsa-java/tree/0.14.1) (2020-11-05) 118 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.14.0...0.14.1) 119 | 120 | **Security** 121 | - Update dependencies [\#103](https://github.com/auth0/jwks-rsa-java/pull/103) ([jimmyjames](https://github.com/jimmyjames)) 122 | 123 | ## [0.14.0](https://github.com/auth0/jwks-rsa-java/tree/0.14.0) (2020-09-29) 124 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.13.0...0.14.0) 125 | 126 | **Added** 127 | - Add proxy [\#94](https://github.com/auth0/jwks-rsa-java/pull/94) ([JosephWitthuhnTR](https://github.com/JosephWitthuhnTR)) 128 | 129 | ## [0.13.0](https://github.com/auth0/jwks-rsa-java/tree/0.13.0) (2020-08-26) 130 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.12.0...0.13.0) 131 | 132 | **Removed** 133 | - remove commons-io dependency [\#95](https://github.com/auth0/jwks-rsa-java/pull/95) ([Jaxsun](https://github.com/Jaxsun)) 134 | 135 | ## [0.12.0](https://github.com/auth0/jwks-rsa-java/tree/0.12.0) (2020-06-25) 136 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.11.0...0.12.0) 137 | 138 | **Added** 139 | - Add NetworkException, docs and tests [\#89](https://github.com/auth0/jwks-rsa-java/pull/89) ([lbalmaceda](https://github.com/lbalmaceda)) 140 | 141 | ## [0.11.0](https://github.com/auth0/jwks-rsa-java/tree/0.11.0) (2020-02-14) 142 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.10.0...0.11.0) 143 | 144 | **Changed** 145 | - Update Gradle version to 6.1.1 [\#84](https://github.com/auth0/jwks-rsa-java/pull/84) ([PaulDaviesC](https://github.com/PaulDaviesC)) 146 | 147 | ## [0.10.0](https://github.com/auth0/jwks-rsa-java/tree/0.10.0) (2020-02-11) 148 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.9.0...0.10.0) 149 | 150 | **Changed** 151 | - Set default cache expiration time to 10 minutes [\#82](https://github.com/auth0/jwks-rsa-java/pull/82) ([jimmyjames](https://github.com/jimmyjames)) 152 | 153 | ## [0.9.0](https://github.com/auth0/jwks-rsa-java/tree/0.9.0) (2019-09-26) 154 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.8.3...0.9.0) 155 | 156 | **Changed** 157 | - Improve ObjectMapper use [\#68](https://github.com/auth0/jwks-rsa-java/pull/68) ([skjolber](https://github.com/skjolber)) 158 | - Concatenate JWKS path with existing path on domain if present [\#67](https://github.com/auth0/jwks-rsa-java/pull/67) ([ltj](https://github.com/ltj)) 159 | 160 | **Security** 161 | - Update jackson-databind to address CVE [\#72](https://github.com/auth0/jwks-rsa-java/pull/72) ([jimmyjames](https://github.com/jimmyjames)) 162 | 163 | ## [0.8.3](https://github.com/auth0/jwks-rsa-java/tree/0.8.3) (2019-08-15) 164 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.8.2...0.8.3) 165 | 166 | **Security** 167 | - Update dependencies [\#65](https://github.com/auth0/jwks-rsa-java/pull/65) ([jimmyjames](https://github.com/jimmyjames)) 168 | 169 | ## [0.8.2](https://github.com/auth0/jwks-rsa-java/tree/0.8.2) (2019-05-22) 170 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.8.1...0.8.2) 171 | 172 | **Security** 173 | - Fix security issue with jackson-databind [\#63](https://github.com/auth0/jwks-rsa-java/pull/63) ([lbalmaceda](https://github.com/lbalmaceda)) 174 | 175 | ## [0.8.1](https://github.com/auth0/jwks-rsa-java/tree/0.8.1) (2019-05-02) 176 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.8.0...0.8.1) 177 | 178 | **Fixed** 179 | - Add content-type header to the jwks request [\#59](https://github.com/auth0/jwks-rsa-java/pull/59) ([lbalmaceda](https://github.com/lbalmaceda)) 180 | 181 | ## [0.8.0](https://github.com/auth0/jwks-rsa-java/tree/0.8.0) (2019-03-28) 182 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.7.0...0.8.0) 183 | 184 | **Added** 185 | - Expose getAll() publicly on UrlJwkProvider [\#38](https://github.com/auth0/jwks-rsa-java/pull/38) ([kampka](https://github.com/kampka)) 186 | - change visibility to public on fromValues method in Jwk [\#36](https://github.com/auth0/jwks-rsa-java/pull/36) ([underscorenico](https://github.com/underscorenico)) 187 | 188 | **Changed** 189 | - Update guava to version 27.0.1-jre [\#54](https://github.com/auth0/jwks-rsa-java/pull/54) ([golszewski86](https://github.com/golszewski86)) 190 | 191 | ## [0.7.0](https://github.com/auth0/jwks-rsa-java/tree/0.7.0) (2019-01-03) 192 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.6.1...0.7.0) 193 | 194 | **Changed** 195 | - Throw correct exception when key is not of type RSA [\#48](https://github.com/auth0/jwks-rsa-java/pull/48) ([lbalmaceda](https://github.com/lbalmaceda)) 196 | 197 | **Security** 198 | - Bump jackson-databind to patch security issues [\#49](https://github.com/auth0/jwks-rsa-java/pull/49) ([lbalmaceda](https://github.com/lbalmaceda)) 199 | 200 | ## [0.6.1](https://github.com/auth0/jwks-rsa-java/tree/0.6.1) (2018-10-24) 201 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.6.0...0.6.1) 202 | 203 | **Security** 204 | - Use latest jackson-databind dependency [\#43](https://github.com/auth0/jwks-rsa-java/pull/43) ([lbalmaceda](https://github.com/lbalmaceda)) 205 | 206 | ## [0.6.0](https://github.com/auth0/jwks-rsa-java/tree/0.6.0) (2018-07-18) 207 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.5.0...0.6.0) 208 | 209 | **Changed** 210 | - Optional kid on single item JWK sets [\#32](https://github.com/auth0/jwks-rsa-java/pull/32) ([lbalmaceda](https://github.com/lbalmaceda)) 211 | 212 | ## [0.5.0](https://github.com/auth0/jwks-rsa-java/tree/0.5.0) (2018-06-13) 213 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/0.4.0...0.5.0) 214 | **Closed issues** 215 | - Improve release procedure [\#29](https://github.com/auth0/jwks-rsa-java/issues/29) 216 | 217 | **Added** 218 | - Optional support for connection / read timeout [\#27](https://github.com/auth0/jwks-rsa-java/pull/27) ([skjolber](https://github.com/skjolber)) 219 | 220 | **Fixed** 221 | - Improve release procedure [\#30](https://github.com/auth0/jwks-rsa-java/pull/30) ([lbalmaceda](https://github.com/lbalmaceda)) 222 | 223 | **Security** 224 | - [Snyk] Fix for 6 vulnerable dependencies [\#28](https://github.com/auth0/jwks-rsa-java/pull/28) ([crew-security](https://github.com/crew-security)) 225 | - bump commons-io due to security vulnerabilities in that library [\#26](https://github.com/auth0/jwks-rsa-java/pull/26) ([ryber](https://github.com/ryber)) 226 | 227 | ## [0.4.0](https://github.com/auth0/jwks-rsa-java/tree/jwks-rsa-0.4.0) (2018-04-27) 228 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/jwks-rsa-0.3.0...jwks-rsa-0.4.0) 229 | 230 | **Added** 231 | - Added url constructor to JwkProviderBuilder [\#22](https://github.com/auth0/jwks-rsa-java/pull/22) ([darthvalinor](https://github.com/darthvalinor)) 232 | 233 | ## [0.3.0](https://github.com/auth0/jwks-rsa-java/tree/jwks-rsa-0.3.0) (2017-11-10) 234 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/jwks-rsa-0.2.0...jwks-rsa-0.3.0) 235 | 236 | **Changed** 237 | - Parse 'key_ops' as an Array rather than a String [\#13](https://github.com/auth0/jwks-rsa-java/pull/13) ([chadramsey](https://github.com/chadramsey)) 238 | - Remove algorithm (alg tag) from mandatory Jwk attributes [\#10](https://github.com/auth0/jwks-rsa-java/pull/10) ([Colin-b](https://github.com/Colin-b)) 239 | 240 | ## [0.2.0](https://github.com/auth0/jwks-rsa-java/tree/jwks-rsa-0.2.0) (2016-12-05) 241 | [Full Changelog](https://github.com/auth0/jwks-rsa-java/compare/jwks-rsa-0.1.0...jwks-rsa-0.2.0) 242 | 243 | **Added** 244 | - Add Rate Limit provider [\#1](https://github.com/auth0/jwks-rsa-java/pull/1) ([lbalmaceda](https://github.com/lbalmaceda)) 245 | 246 | **Changed** 247 | - Refactor JwkProviderBuilder [\#2](https://github.com/auth0/jwks-rsa-java/pull/2) ([lbalmaceda](https://github.com/lbalmaceda)) 248 | - Replace ExecutorService with primitive counters. [\#3](https://github.com/auth0/jwks-rsa-java/pull/3) ([lbalmaceda](https://github.com/lbalmaceda)) 249 | 250 | ## [0.1.0](https://github.com/auth0/jwks-rsa-java/tree/jwks-rsa-0.1.0) (2016-08-30) 251 | 252 | JSON Web Token Set parser library for Java. Initial release. 253 | 254 | 255 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples using jwks-rsa-java 2 | 3 | - [Provider configuration](#provider-configuration) 4 | - [Error handling](#error-handling) 5 | 6 | ## Provider configuration 7 | 8 | This library contains several `JwkProvider` implementations, which is used to obtain a JWK. 9 | 10 | The `JwkProviderBuilder` is the preferred way to create a `JwkProvider`. Configurations can be combined to suit your needs. 11 | 12 | ### Cache retrieved JWK 13 | 14 | To create a provider for domain `https://samples.auth0.com` that will cache a JWK using an LRU in-memory cache: 15 | 16 | ```java 17 | JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") 18 | // cache up to 10 JWKs for up to 24 hours 19 | .cached(10, 24, TimeUnit.HOURS) 20 | .build(); 21 | ``` 22 | 23 | ### Configure rate limits 24 | 25 | `RateLimitJwkProvider` will limit the amounts of different signing keys to get in a given time frame. 26 | 27 | > By default the rate is limited to 10 different keys per minute but these values can be changed. 28 | 29 | ```java 30 | JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") 31 | // up to 10 JWKs can be retrieved within one minute 32 | .rateLimited(10, 1, TimeUnit.MINUTES) 33 | .build(); 34 | ``` 35 | 36 | ### Configure network timeout settings 37 | 38 | The connect and read network timeouts can be configured using the builder: 39 | 40 | ```java 41 | JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") 42 | // Connect timeout of 1 second, read timeout of 2 seconds (values are in milliseconds) 43 | .timeouts(1000, 2000) 44 | .build(); 45 | ``` 46 | 47 | See the [JwkProviderBuilder JavaDocs](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/com/auth0/jwk/JwkProviderBuilder.html) for all available configurations. 48 | 49 | ## Error handling 50 | 51 | There are certain scenarios in which this library can fail. Read below to understand what to expect and how to handle the errors. 52 | 53 | ### Missing JSON Web Key 54 | This error may arise when the hosted JSON Web Key set (JWKS) file doesn't represent a valid set of keys, or is empty. 55 | They are raised as a `SigningKeyNotFoundException`. The cause should to be inspected in order to understand the specific failure reason. 56 | 57 | #### Network error 58 | There's a special case for Network errors. These errors represent timeouts, invalid URLs, or a faulty internet connection. 59 | They may occur when fetching the keys from the given URL. They are raised as a `NetworkException` instance. 60 | 61 | If you need to detect this scenario, make sure to check it before the catch of `SigningKeyNotFoundException`. 62 | 63 | ```java 64 | try { 65 | // ... 66 | } catch (NetworkException e) { 67 | // Network error 68 | } catch (SigningKeyNotFoundException e) { 69 | // Key is invalid or not found 70 | } 71 | ``` 72 | 73 | ### Unsupported JSON Web Key 74 | When the received key is not of a supported type, or the attribute values representing it are wrong, an `InvalidPublicKeyException` will be raised. 75 | The following key types are supported: 76 | - RSA 77 | - Elliptic Curve 78 | - P-256 79 | - P-384 80 | - P-521 81 | 82 | ### Rate limits 83 | When using a rate-limited provider, a `RateLimitReachedException` error will be raised when the limit is breached. 84 | The exception can help determine how long to wait until the next call is available. 85 | 86 | ```java 87 | try { 88 | // ... 89 | } catch (RateLimitReachedException e) { 90 | long waitTime = e.getAvailableIn() 91 | // wait until available 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![A Java library for obtaining JSON Web Keys from a JWKS (JSON Web Key Set) endpoint.](https://cdn.auth0.com/website/sdks/banners/jwks-rsa-java-banner.png) 2 | 3 | ![Build Status](https://img.shields.io/github/checks-status/auth0/jwks-rsa-java/master) 4 | [![Coverage Status](https://codecov.io/gh/auth0/jwks-rsa-java/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/github/auth0/jwks-rsa-java) 5 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](https://doge.mit-license.org/) 6 | [![Maven Central](https://img.shields.io/maven-central/v/com.auth0/jwks-rsa.svg?style=flat-square)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%20com.auth0%20a%3Ajwks-rsa) 7 | [![javadoc](https://javadoc.io/badge2/com.auth0/jwks-rsa-java/javadoc.svg)](https://javadoc.io/doc/com.auth0/jwks-rsa) 8 | 9 | > **Note** 10 | > 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. 11 | > 12 | > 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. 13 | 14 | :books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :computer: [API Reference](#api-reference) :speech_balloon: [Feedback](#feedback) 15 | 16 | ## Documentation 17 | - [Examples](./EXAMPLES.md) - code samples for common jwks-rsa-java scenarios. 18 | - [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0. 19 | 20 | ## Getting Started 21 | 22 | ### Requirements 23 | 24 | Java 8 or above. 25 | 26 | ### Installation 27 | 28 | Add the dependency via Maven: 29 | 30 | ```xml 31 | 32 | com.auth0 33 | jwks-rsa 34 | 0.22.1 35 | 36 | ``` 37 | 38 | or Gradle: 39 | 40 | ```gradle 41 | implementation 'com.auth0:jwks-rsa:0.22.1' 42 | ``` 43 | 44 | ### Usage 45 | 46 | The JSON Web Tokens you obtain from an authorization server include a [key id](https://tools.ietf.org/html/rfc7515#section-4.1.4) header parameter ("kid"), used to uniquely identify the Key used to sign the token. 47 | 48 | Given the following JWT: 49 | 50 | ``` 51 | eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJrSTVNakk1T1VZNU9EYzFOMFE0UXpNME9VWXpOa1ZHTVRKRE9VRXpRa0ZDT1RVM05qRTJSZyJ9.eyJpc3MiOiJodHRwczovL3NhbmRyaW5vLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1NjMyNTAxZjQ2OGYwZjE3NTZmNGNhYjAiLCJhdWQiOiJQN2JhQnRTc3JmQlhPY3A5bHlsMUZEZVh0ZmFKUzRyViIsImV4cCI6MTQ2ODk2NDkyNiwiaWF0IjoxNDY4OTI4OTI2fQ.NaNeRSDCNu522u4hcVhV65plQOiGPStgSzVW4vR0liZYQBlZ_3OKqCmHXsu28NwVHW7_KfVgOz4m3BK6eMDZk50dAKf9LQzHhiG8acZLzm5bNMU3iobSAJdRhweRht544ZJkzJ-scS1fyI4gaPS5aD3SaLRYWR0Xsb6N1HU86trnbn-XSYSspNqzIUeJjduEpPwC53V8E2r1WZXbqEHwM9_BGEeNTQ8X9NqCUvbQtnylgYR3mfJRL14JsCWNFmmamgNNHAI0uAJo84mu_03I25eVuCK0VYStLPd0XFEyMVFpk48Bg9KNWLMZ7OUGTB_uv_1u19wKYtqeTbt9m1YcPMQ 52 | ``` 53 | 54 | Decode it using a JWT library or tool like [jwt.io](https://jwt.io/?value=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlJrSTVNakk1T1VZNU9EYzFOMFE0UXpNME9VWXpOa1ZHTVRKRE9VRXpRa0ZDT1RVM05qRTJSZyJ9.eyJpc3MiOiJodHRwczovL3NhbmRyaW5vLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw1NjMyNTAxZjQ2OGYwZjE3NTZmNGNhYjAiLCJhdWQiOiJQN2JhQnRTc3JmQlhPY3A5bHlsMUZEZVh0ZmFKUzRyViIsImV4cCI6MTQ2ODk2NDkyNiwiaWF0IjoxNDY4OTI4OTI2fQ.NaNeRSDCNu522u4hcVhV65plQOiGPStgSzVW4vR0liZYQBlZ_3OKqCmHXsu28NwVHW7_KfVgOz4m3BK6eMDZk50dAKf9LQzHhiG8acZLzm5bNMU3iobSAJdRhweRht544ZJkzJ-scS1fyI4gaPS5aD3SaLRYWR0Xsb6N1HU86trnbn-XSYSspNqzIUeJjduEpPwC53V8E2r1WZXbqEHwM9_BGEeNTQ8X9NqCUvbQtnylgYR3mfJRL14JsCWNFmmamgNNHAI0uAJo84mu_03I25eVuCK0VYStLPd0XFEyMVFpk48Bg9KNWLMZ7OUGTB_uv_1u19wKYtqeTbt9m1YcPMQ) and extract the `kid` parameter from the Header claims. 55 | 56 | ```json 57 | { 58 | "typ": "JWT", 59 | "alg": "RS256", 60 | "kid": "RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg" 61 | } 62 | ``` 63 | 64 | The `kid` value can then be used to obtain the JWK using a `JwkProvider`. 65 | 66 | Create a `JWKProvider` using the domain from which to fetch the JWK. The provider will use the domain to build the URL `https:{your-domain}/.well-known/jwks.json`: 67 | 68 | ```java 69 | JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") 70 | .build(); 71 | ``` 72 | 73 | A `Jwk` can be obtained using the `get(String keyId)` method: 74 | 75 | ```java 76 | Jwk jwk = provider.get("{kid of the signing key}"); // throws Exception when not found or can't get one 77 | ``` 78 | 79 | The provider can be configured to cache JWKs to avoid unnecessary network requests, as well as only fetch the JWKs within a defined rate limit: 80 | 81 | ```java 82 | JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/") 83 | // up to 10 JWKs will be cached for up to 24 hours 84 | .cached(10, 24, TimeUnit.HOURS) 85 | // up to 10 JWKs can be retrieved within one minute 86 | .rateLimited(10, 1, TimeUnit.MINUTES) 87 | .build(); 88 | ``` 89 | 90 | See the [examples](./EXAMPLES.md) for additional configurations. 91 | 92 | ## API Reference 93 | 94 | - [jwks-rsa-java JavaDocs](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/) 95 | 96 | ## Feedback 97 | 98 | ### Contributing 99 | 100 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: 101 | 102 | - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 103 | - [Auth0's code of conduct guidelines]((https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)) 104 | 105 | ### Raise an issue 106 | To provide feedback or report a bug, [please raise an issue on our issue tracker](https://github.com/auth0/jwks-rsa-java/issues). 107 | 108 | ### Vulnerability Reporting 109 | 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. 110 | 111 | --- 112 | 113 |

114 | 115 | 116 | 117 | Auth0 Logo 118 | 119 |

120 |

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

121 |

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

123 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'jacoco' 4 | id 'com.auth0.gradle.oss-library.java' 5 | } 6 | 7 | group = 'com.auth0' 8 | 9 | logger.lifecycle("Using version ${version} for ${name} group $group") 10 | 11 | def signingKey = findProperty('signingKey') 12 | def signingKeyPwd = findProperty('signingPassword') 13 | 14 | oss { 15 | name 'jwks-rsa' 16 | repository 'jwks-rsa-java' 17 | organization 'auth0' 18 | description 'JSON Web Key Set parser library' 19 | baselineCompareVersion '0.15.0' 20 | skipAssertSigningConfiguration true 21 | 22 | developers { 23 | auth0 { 24 | displayName = 'Auth0' 25 | email = 'oss@auth0.com' 26 | } 27 | lbalmaceda { 28 | displayName = 'Luciano Balmaceda' 29 | email = 'luciano.balmaceda@auth0.com' 30 | } 31 | hzalaz { 32 | displayName = 'Hernan Zalazar' 33 | email = 'hernan@auth0.com' 34 | } 35 | } 36 | } 37 | 38 | signing { 39 | useInMemoryPgpKeys(signingKey, signingKeyPwd) 40 | } 41 | 42 | jacocoTestReport { 43 | reports { 44 | xml.enabled = true 45 | html.enabled = true 46 | } 47 | } 48 | 49 | java { 50 | toolchain { 51 | languageVersion = JavaLanguageVersion.of(8) 52 | } 53 | } 54 | 55 | compileJava { 56 | sourceCompatibility '1.8' 57 | targetCompatibility '1.8' 58 | } 59 | 60 | test { 61 | testLogging { 62 | events "skipped", "failed" 63 | exceptionFormat "short" 64 | } 65 | } 66 | 67 | repositories { 68 | mavenCentral() 69 | } 70 | 71 | dependencies { 72 | implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version:'2.15.0' 73 | implementation (group: 'com.google.guava', name: 'guava', version:'32.1.1-jre') { 74 | // needed due to https://github.com/google/guava/issues/6654 75 | exclude group: "org.mockito", module: "mockito-core" 76 | } 77 | testImplementation group: 'junit', name: 'junit', version:'4.13.1' 78 | testImplementation group: 'org.mockito', name: 'mockito-core', version:'1.10.19' 79 | testImplementation group: 'org.hamcrest', name: 'hamcrest-library', version:'1.3' 80 | } 81 | 82 | // See https://github.com/google/guava/releases/tag/v32.1.0 for why this is required 83 | sourceSets.all { 84 | configurations.getByName(runtimeClasspathConfigurationName) { 85 | attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") 86 | } 87 | configurations.getByName(compileClasspathConfigurationName) { 88 | attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") 89 | } 90 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | status: 6 | project: 7 | default: 8 | enabled: true 9 | target: auto 10 | threshold: 3% 11 | if_no_uploads: error 12 | patch: 13 | default: 14 | enabled: true 15 | target: 80% 16 | threshold: 25% 17 | if_no_uploads: error 18 | changes: 19 | default: 20 | enabled: true 21 | if_no_uploads: error 22 | comment: false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0/jwks-rsa-java/d877bd7e74510d905ded29468d8b8b7b6f9703ef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_sdks 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | } 6 | plugins { 7 | id 'com.auth0.gradle.oss-library.java' version '0.18.0' 8 | } 9 | } 10 | rootProject.name = 'jwks-rsa' 11 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/Bucket.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | /** 4 | * Token Bucket interface. 5 | */ 6 | interface Bucket { 7 | 8 | /** 9 | * Calculates the wait time before one token will be available in the Bucket. 10 | * 11 | * @return the wait time in milliseconds in which one token will be available in the Bucket. 12 | */ 13 | long willLeakIn(); 14 | 15 | /** 16 | * Calculates the wait time before the given amount of tokens will be available in the Bucket. 17 | * 18 | * @param count the amount of tokens to check how much time to wait for. 19 | * @return the wait time in milliseconds in which the given amount of tokens will be available in the Bucket. 20 | */ 21 | long willLeakIn(long count); 22 | 23 | /** 24 | * Tries to consume one token from the Bucket. 25 | * 26 | * @return true if it could consume the token or false if the Bucket doesn't have tokens available now. 27 | */ 28 | boolean consume(); 29 | 30 | /** 31 | * Tries to consume the given amount of tokens from the Bucket. 32 | * 33 | * @param count the amount of tokens to try to consume. 34 | * @return true if it could consume the given amount of tokens or false if the Bucket doesn't have that amount of tokens available now. 35 | */ 36 | boolean consume(long count); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/BucketImpl.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | /** 6 | * Token Bucket implementation to guarantee availability of a fixed amount of tokens in a given time rate. 7 | */ 8 | class BucketImpl implements Bucket { 9 | 10 | private final long size; 11 | private final long rate; 12 | private final TimeUnit rateUnit; 13 | private long available; 14 | private long accumDelta; 15 | private long startTime; 16 | 17 | BucketImpl(long size, long rate, TimeUnit rateUnit) { 18 | assertPositiveValue(size, "Invalid bucket size."); 19 | assertPositiveValue(rate, "Invalid bucket refill rate."); 20 | this.size = size; 21 | this.available = size; 22 | this.rate = rate; 23 | this.rateUnit = rateUnit; 24 | this.startTime = System.nanoTime(); 25 | } 26 | 27 | private void assertPositiveValue(long value, long maxValue, String exceptionMessage) { 28 | if (value < 1 || value > maxValue) { 29 | throw new IllegalArgumentException(exceptionMessage); 30 | } 31 | } 32 | 33 | private void assertPositiveValue(Number value, String exceptionMessage) { 34 | this.assertPositiveValue(value.intValue(), value.intValue(), exceptionMessage); 35 | } 36 | 37 | @Override 38 | public synchronized long willLeakIn() { 39 | return willLeakIn(1); 40 | } 41 | 42 | @Override 43 | public synchronized long willLeakIn(long count) { 44 | assertPositiveValue(count, size, String.format("Cannot consume %d tokens when the BucketImpl size is %d!", count, size)); 45 | updateAvailableTokens(); 46 | if (available >= count) { 47 | return 0; 48 | } 49 | 50 | long leakDelta = getTimeSinceLastTokenAddition(); 51 | if (leakDelta < getRatePerToken()) { 52 | leakDelta = getRatePerToken() - leakDelta; 53 | } 54 | final long remaining = count - available - 1; 55 | if (remaining > 0) { 56 | leakDelta += getRatePerToken() * remaining; 57 | } 58 | return leakDelta; 59 | } 60 | 61 | @Override 62 | public synchronized boolean consume() { 63 | return consume(1); 64 | } 65 | 66 | @Override 67 | public synchronized boolean consume(long count) { 68 | assertPositiveValue(count, size, String.format("Cannot consume %d tokens when the BucketImpl size is %d!", count, size)); 69 | updateAvailableTokens(); 70 | 71 | if (count <= available) { 72 | available -= count; 73 | return true; 74 | } 75 | return false; 76 | } 77 | 78 | private void updateAvailableTokens() { 79 | final long ratePerToken = getRatePerToken(); 80 | final long elapsed = getTimeSinceLastTokenAddition(); 81 | if (elapsed < ratePerToken) { 82 | return; 83 | } 84 | 85 | accumDelta = elapsed % ratePerToken; 86 | long count = elapsed / ratePerToken; 87 | if (count > size - available) { 88 | count = size - available; 89 | } 90 | if (count > 0) { 91 | available += count; 92 | } 93 | restartStopWatch(); 94 | } 95 | 96 | private void restartStopWatch() { 97 | startTime = System.nanoTime(); 98 | } 99 | 100 | private long getTimeSinceLastTokenAddition() { 101 | long elapsedTime = System.nanoTime() - startTime; 102 | return TimeUnit.MILLISECONDS.convert(elapsedTime, TimeUnit.NANOSECONDS) + accumDelta; 103 | } 104 | 105 | private long getRatePerToken() { 106 | return rateUnit.toMillis(rate); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/GuavaCachedJwkProvider.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import com.google.common.cache.Cache; 4 | import com.google.common.cache.CacheBuilder; 5 | 6 | import java.time.Duration; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * Jwk provider that caches previously obtained Jwk in memory using a Google Guava cache 12 | */ 13 | @SuppressWarnings("WeakerAccess") 14 | public class GuavaCachedJwkProvider implements JwkProvider { 15 | 16 | private final Cache cache; 17 | private final JwkProvider provider; 18 | @VisibleForTesting 19 | static final String NULL_KID_KEY = "null-kid"; 20 | 21 | /** 22 | * Creates a new provider that will cache up to 5 jwks for at most 10 minutes 23 | * 24 | * @param provider fallback provider to use when jwk is not cached 25 | */ 26 | public GuavaCachedJwkProvider(final JwkProvider provider) { 27 | this(provider, 5, Duration.ofMinutes(10)); 28 | } 29 | 30 | /** 31 | * Creates a new cached provider specifying cache size and ttl 32 | * 33 | * @param provider fallback provider to use when jwk is not cached 34 | * @param size number of jwk to cache 35 | * @param expiresIn amount of time a jwk will live in the cache 36 | * @param expiresUnit unit of the expiresIn parameter 37 | */ 38 | public GuavaCachedJwkProvider(final JwkProvider provider, long size, long expiresIn, TimeUnit expiresUnit) { 39 | this.provider = provider; 40 | this.cache = CacheBuilder.newBuilder() 41 | .maximumSize(size) 42 | // configure using timeunit; see https://github.com/auth0/jwks-rsa-java/issues/136 43 | .expireAfterWrite(expiresIn, expiresUnit) 44 | .build(); 45 | } 46 | 47 | /** 48 | * Creates a new cached provider specifying cache size and ttl 49 | * 50 | * @param provider fallback provider to use when jwk is not cached 51 | * @param size number of jwt to cache 52 | * @param expiresIn amount of time a jwk will live in the cache 53 | */ 54 | public GuavaCachedJwkProvider(final JwkProvider provider, long size, Duration expiresIn) { 55 | this(provider, size, expiresIn.toMillis(), TimeUnit.MILLISECONDS); 56 | } 57 | 58 | @Override 59 | public Jwk get(final String keyId) throws JwkException { 60 | try { 61 | String cacheKey = keyId == null ? NULL_KID_KEY : keyId; 62 | return cache.get(cacheKey, () -> provider.get(keyId)); 63 | } catch (ExecutionException e) { 64 | // throw the proper exception directly, see https://github.com/auth0/jwks-rsa-java/issues/165 65 | // cause should always be JwkException, but check just to be safe 66 | if (e.getCause() instanceof JwkException) { 67 | throw (JwkException) e.getCause(); 68 | } 69 | // If somehow cause is not JwkException, just wrap 70 | throw new JwkException("Unable to obtain key with kid " + keyId, e); 71 | } 72 | } 73 | 74 | @VisibleForTesting 75 | JwkProvider getBaseProvider() { 76 | return provider; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/InvalidPublicKeyException.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @SuppressWarnings("WeakerAccess") 4 | public class InvalidPublicKeyException extends JwkException { 5 | 6 | public InvalidPublicKeyException(String msg) { 7 | super(msg); 8 | } 9 | 10 | public InvalidPublicKeyException(String msg, Throwable cause) { 11 | super(msg, cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/Jwk.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import java.math.BigInteger; 4 | import java.security.AlgorithmParameters; 5 | import java.security.KeyFactory; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.PublicKey; 8 | import java.security.spec.ECGenParameterSpec; 9 | import java.security.spec.ECParameterSpec; 10 | import java.security.spec.ECPoint; 11 | import java.security.spec.ECPublicKeySpec; 12 | import java.security.spec.InvalidKeySpecException; 13 | import java.security.spec.InvalidParameterSpecException; 14 | import java.security.spec.RSAPublicKeySpec; 15 | import java.util.Base64; 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.HashMap; 20 | 21 | /** 22 | * Represents a JSON Web Key (JWK) used to verify the signature of JWTs 23 | */ 24 | @SuppressWarnings("WeakerAccess") 25 | public class Jwk { 26 | private static final String ALGORITHM_RSA = "RSA"; 27 | private static final String ALGORITHM_ELLIPTIC_CURVE = "EC"; 28 | private static final String ELLIPTIC_CURVE_TYPE_P256 = "P-256"; 29 | private static final String ELLIPTIC_CURVE_TYPE_P384 = "P-384"; 30 | private static final String ELLIPTIC_CURVE_TYPE_P521 = "P-521"; 31 | 32 | private final String id; 33 | private final String type; 34 | private final String algorithm; 35 | private final String usage; 36 | private final List operations; 37 | private final String certificateUrl; 38 | private final List certificateChain; 39 | private final String certificateThumbprint; 40 | private final Map additionalAttributes; 41 | 42 | /** 43 | * Creates a new Jwk 44 | * 45 | * @param id kid 46 | * @param type kty 47 | * @param algorithm alg 48 | * @param usage use 49 | * @param operations key_ops 50 | * @param certificateUrl x5u 51 | * @param certificateChain x5c 52 | * @param certificateThumbprint x5t 53 | * @param additionalAttributes additional attributes not part of the standard ones 54 | */ 55 | @SuppressWarnings("WeakerAccess") 56 | public Jwk(String id, String type, String algorithm, String usage, List operations, String certificateUrl, List certificateChain, String certificateThumbprint, Map additionalAttributes) { 57 | this.id = id; 58 | this.type = type; 59 | this.algorithm = algorithm; 60 | this.usage = usage; 61 | this.operations = operations; 62 | this.certificateUrl = certificateUrl; 63 | this.certificateChain = certificateChain; 64 | this.certificateThumbprint = certificateThumbprint; 65 | this.additionalAttributes = additionalAttributes; 66 | } 67 | 68 | /** 69 | * Creates a new Jwk 70 | * 71 | * @param id 72 | * @param type 73 | * @param algorithm 74 | * @param usage 75 | * @param operations 76 | * @param certificateUrl 77 | * @param certificateChain 78 | * @param certificateThumbprint 79 | * @param additionalAttributes 80 | * @deprecated The specification states that the 'key_ops' (operations) parameter contains an array value. 81 | * Use {@link #Jwk(String, String, String, String, List, String, List, String, Map)} 82 | */ 83 | @Deprecated 84 | @SuppressWarnings("WeakerAccess") 85 | public Jwk(String id, String type, String algorithm, String usage, String operations, String certificateUrl, List certificateChain, String certificateThumbprint, Map additionalAttributes) { 86 | this(id, type, algorithm, usage, Collections.singletonList(operations), certificateUrl, certificateChain, certificateThumbprint, additionalAttributes); 87 | } 88 | 89 | @SuppressWarnings("unchecked") 90 | public static Jwk fromValues(Map map) { 91 | Map values = new HashMap<>(map); 92 | String kid = (String) values.remove("kid"); 93 | String kty = (String) values.remove("kty"); 94 | String alg = (String) values.remove("alg"); 95 | String use = (String) values.remove("use"); 96 | Object keyOps = values.remove("key_ops"); 97 | String x5u = (String) values.remove("x5u"); 98 | List x5c = (List) values.remove("x5c"); 99 | String x5t = (String) values.remove("x5t"); 100 | if (kty == null) { 101 | throw new IllegalArgumentException("Attributes " + map + " are not from a valid jwk"); 102 | } 103 | if (keyOps instanceof String) { 104 | return new Jwk(kid, kty, alg, use, (String) keyOps, x5u, x5c, x5t, values); 105 | } else { 106 | return new Jwk(kid, kty, alg, use, (List) keyOps, x5u, x5c, x5t, values); 107 | } 108 | } 109 | 110 | @SuppressWarnings("WeakerAccess") 111 | public String getId() { 112 | return id; 113 | } 114 | 115 | @SuppressWarnings("WeakerAccess") 116 | public String getType() { 117 | return type; 118 | } 119 | 120 | @SuppressWarnings("WeakerAccess") 121 | public String getAlgorithm() { 122 | return algorithm; 123 | } 124 | 125 | @SuppressWarnings("WeakerAccess") 126 | public String getUsage() { 127 | return usage; 128 | } 129 | 130 | @SuppressWarnings("WeakerAccess") 131 | public String getOperations() { 132 | if (operations == null || operations.isEmpty()) { 133 | return null; 134 | } 135 | StringBuilder sb = new StringBuilder(); 136 | String delimiter = ","; 137 | for (String operation : operations) { 138 | sb.append(operation); 139 | sb.append(delimiter); 140 | } 141 | String ops = sb.toString(); 142 | return ops.substring(0, ops.length() - delimiter.length()); 143 | } 144 | 145 | @SuppressWarnings("WeakerAccess") 146 | public List getOperationsAsList() { 147 | return operations; 148 | } 149 | 150 | @SuppressWarnings("WeakerAccess") 151 | public String getCertificateUrl() { 152 | return certificateUrl; 153 | } 154 | 155 | @SuppressWarnings("WeakerAccess") 156 | public List getCertificateChain() { 157 | return certificateChain; 158 | } 159 | 160 | @SuppressWarnings("WeakerAccess") 161 | public String getCertificateThumbprint() { 162 | return certificateThumbprint; 163 | } 164 | 165 | public Map getAdditionalAttributes() { 166 | return additionalAttributes; 167 | } 168 | 169 | /** 170 | * Returns a {@link PublicKey} if the {@code 'alg'} is {@code 'RSA'} or {@code 'EC'} 171 | * 172 | * @return a public key 173 | * @throws InvalidPublicKeyException if the key cannot be built or the key type is not a supported type of RSA or EC 174 | */ 175 | @SuppressWarnings("WeakerAccess") 176 | public PublicKey getPublicKey() throws InvalidPublicKeyException { 177 | PublicKey publicKey = null; 178 | 179 | switch (type) { 180 | case ALGORITHM_RSA: 181 | try { 182 | KeyFactory kf = KeyFactory.getInstance(ALGORITHM_RSA); 183 | BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("n"))); 184 | BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("e"))); 185 | publicKey = kf.generatePublic(new RSAPublicKeySpec(modulus, exponent)); 186 | } catch (InvalidKeySpecException e) { 187 | throw new InvalidPublicKeyException("Invalid public key", e); 188 | } catch (NoSuchAlgorithmException e) { 189 | throw new InvalidPublicKeyException("Invalid algorithm to generate key", e); 190 | } 191 | break; 192 | 193 | case ALGORITHM_ELLIPTIC_CURVE: 194 | try { 195 | KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_ELLIPTIC_CURVE); 196 | ECPoint ecPoint = new ECPoint(new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("x"))), 197 | new BigInteger(1, Base64.getUrlDecoder().decode(stringValue("y")))); 198 | AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(ALGORITHM_ELLIPTIC_CURVE); 199 | 200 | String curve = stringValue("crv"); 201 | switch (curve) { 202 | case ELLIPTIC_CURVE_TYPE_P256: 203 | algorithmParameters.init(new ECGenParameterSpec("secp256r1")); 204 | break; 205 | case ELLIPTIC_CURVE_TYPE_P384: 206 | algorithmParameters.init(new ECGenParameterSpec("secp384r1")); 207 | break; 208 | case ELLIPTIC_CURVE_TYPE_P521: 209 | algorithmParameters.init(new ECGenParameterSpec("secp521r1")); 210 | break; 211 | default: 212 | throw new InvalidPublicKeyException("Invalid or unsupported curve type " + curve); 213 | } 214 | ECParameterSpec ecParameterSpec = algorithmParameters.getParameterSpec(ECParameterSpec.class); 215 | ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(ecPoint, ecParameterSpec); 216 | publicKey = keyFactory.generatePublic(ecPublicKeySpec); 217 | } catch (NoSuchAlgorithmException e) { 218 | throw new InvalidPublicKeyException("Invalid algorithm to generate key", e); 219 | } catch (InvalidKeySpecException | InvalidParameterSpecException e) { 220 | throw new InvalidPublicKeyException("Invalid public key", e); 221 | } 222 | break; 223 | 224 | default: 225 | throw new InvalidPublicKeyException("The key type of " + type + " is not supported"); 226 | } 227 | 228 | return publicKey; 229 | } 230 | 231 | private String stringValue(String key) { 232 | return (String) additionalAttributes.get(key); 233 | } 234 | 235 | @Override 236 | public String toString() { 237 | return "Jwk{" + 238 | "id='" + id + '\'' + 239 | ", type='" + type + '\'' + 240 | ", algorithm='" + algorithm + '\'' + 241 | ", usage='" + usage + '\'' + 242 | ", additionalAttributes=" + additionalAttributes + 243 | '}'; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/JwkException.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @SuppressWarnings("WeakerAccess") 4 | public class JwkException extends Exception { 5 | 6 | public JwkException(String message) { 7 | super(message); 8 | } 9 | 10 | public JwkException(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/JwkProvider.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | /** 4 | * Provider of Jwk 5 | */ 6 | @SuppressWarnings("WeakerAccess") 7 | public interface JwkProvider { 8 | /** 9 | * Attempts to get a JWK using the Key ID value. Note that implementations are synchronous (blocking). 10 | * 11 | * @param keyId value of the kid found in a JWT 12 | * @return a JWK 13 | * @throws SigningKeyNotFoundException if no jwk can be found using the given kid 14 | */ 15 | Jwk get(String keyId) throws JwkException; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/JwkProviderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import java.net.Proxy; 4 | import java.net.URL; 5 | import java.time.Duration; 6 | import java.util.Map; 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static com.auth0.jwk.UrlJwkProvider.urlForDomain; 10 | 11 | /** 12 | * JwkProvider builder 13 | */ 14 | @SuppressWarnings("WeakerAccess") 15 | public class JwkProviderBuilder { 16 | 17 | private final URL url; 18 | private Proxy proxy; 19 | private Duration expiresIn; 20 | private Integer connectTimeout; 21 | private Integer readTimeout; 22 | private long cacheSize; 23 | private boolean cached; 24 | private BucketImpl bucket; 25 | private boolean rateLimited; 26 | private Map headers; 27 | 28 | /** 29 | * Creates a new Builder with the given URL where to load the jwks from. 30 | * 31 | * @param url to load the jwks 32 | * @throws IllegalStateException if url is null 33 | */ 34 | public JwkProviderBuilder(URL url) { 35 | if (url == null) { 36 | throw new IllegalStateException("Cannot build provider without url to jwks"); 37 | } 38 | this.url = url; 39 | this.cached = true; 40 | this.expiresIn = Duration.ofHours(10); 41 | this.cacheSize = 5; 42 | this.rateLimited = true; 43 | this.bucket = new BucketImpl(10, 1, TimeUnit.MINUTES); 44 | } 45 | 46 | /** 47 | * Creates a new Builder with a domain where to look for the jwks. 48 | *

It can be a url link 'https://samples.auth0.com' or just a domain 'samples.auth0.com'. 49 | * If the protocol (http or https) is not provided then https is used by default. 50 | * The default jwks path "/.well-known/jwks.json" is appended to the given string domain. 51 | *

For example, when the domain is "samples.auth0.com" 52 | * the jwks url that will be used is "https://samples.auth0.com/.well-known/jwks.json" 53 | *

Use {@link #JwkProviderBuilder(URL)} if you need to pass a full URL. 54 | * 55 | * @param domain where jwks is published 56 | * @throws IllegalStateException if domain is null 57 | * @see UrlJwkProvider#UrlJwkProvider(String) 58 | */ 59 | public JwkProviderBuilder(String domain) { 60 | this(buildJwkUrl(domain)); 61 | } 62 | 63 | private static URL buildJwkUrl(String domain) { 64 | if (domain == null) { 65 | throw new IllegalStateException("Cannot build provider without domain"); 66 | } 67 | return urlForDomain(domain); 68 | } 69 | 70 | /** 71 | * Toggle the cache of Jwk. By default, the provider will use a cache size of 5 and a duration of 10 hours. 72 | * 73 | * @param cached if the provider should cache jwks 74 | * @return the builder 75 | */ 76 | public JwkProviderBuilder cached(boolean cached) { 77 | this.cached = cached; 78 | return this; 79 | } 80 | 81 | /** 82 | * Enable the cache specifying size and expire time. 83 | * 84 | * @param cacheSize number of jwk to cache 85 | * @param expiresIn amount of time the jwk will be cached 86 | * @return the builder 87 | */ 88 | public JwkProviderBuilder cached(long cacheSize, Duration expiresIn) { 89 | this.cached = true; 90 | this.cacheSize = cacheSize; 91 | this.expiresIn = expiresIn; 92 | return this; 93 | } 94 | 95 | /** 96 | * Enable the cache specifying size and expire time. 97 | * 98 | * @param cacheSize number of jwk to cache 99 | * @param expiresIn amount of time the jwk will be cached 100 | * @param unit unit of time for the expire of jwk 101 | * @return the builder 102 | */ 103 | public JwkProviderBuilder cached(long cacheSize, long expiresIn, TimeUnit unit) { 104 | return this.cached(cacheSize, Duration.ofSeconds(unit.toSeconds(expiresIn))); 105 | } 106 | 107 | /** 108 | * Toggle the rate limit of Jwk. By default the Provider will use rate limit. 109 | * 110 | * @param rateLimited if the provider should rate limit jwks 111 | * @return the builder 112 | */ 113 | public JwkProviderBuilder rateLimited(boolean rateLimited) { 114 | this.rateLimited = rateLimited; 115 | return this; 116 | } 117 | 118 | /** 119 | * Enable the cache specifying size and expire time. 120 | * 121 | * @param bucketSize max number of jwks to deliver in the given rate. 122 | * @param refillRate amount of time to wait before a jwk can the jwk will be cached 123 | * @param unit unit of time for the expire of jwk 124 | * @return the builder 125 | */ 126 | public JwkProviderBuilder rateLimited(long bucketSize, long refillRate, TimeUnit unit) { 127 | bucket = new BucketImpl(bucketSize, refillRate, unit); 128 | return this; 129 | } 130 | 131 | /** 132 | * Sets the proxy to use for the connection. 133 | * 134 | * @param proxy proxy server to use when making this connection 135 | * @return the builder 136 | */ 137 | public JwkProviderBuilder proxied(Proxy proxy) { 138 | this.proxy = proxy; 139 | return this; 140 | } 141 | 142 | /** 143 | * Sets a custom connect and read timeout values. 144 | * When this method is not called, the default timeout values 145 | * will be those defined by the {@link UrlJwkProvider} implementation. 146 | * 147 | * @param connectTimeout connection timeout in milliseconds. 148 | * @param readTimeout read timeout in milliseconds. 149 | * @see UrlJwkProvider 150 | * @return the builder 151 | */ 152 | public JwkProviderBuilder timeouts(int connectTimeout, int readTimeout) { 153 | this.connectTimeout = connectTimeout; 154 | this.readTimeout = readTimeout; 155 | return this; 156 | } 157 | 158 | /** 159 | * Sets the headers to send on the request. Any headers set here will override the default headers ("Accept": "application/json") 160 | * 161 | * @param headers a map of header keys to values to send on the request. 162 | * @return this builder instance 163 | */ 164 | public JwkProviderBuilder headers(Map headers) { 165 | this.headers = headers; 166 | return this; 167 | } 168 | 169 | /** 170 | * Creates a {@link JwkProvider} 171 | * 172 | * @return a newly created {@link JwkProvider} 173 | */ 174 | public JwkProvider build() { 175 | JwkProvider urlProvider = new UrlJwkProvider(url, connectTimeout, readTimeout, proxy, headers); 176 | if (this.rateLimited) { 177 | urlProvider = new RateLimitedJwkProvider(urlProvider, bucket); 178 | } 179 | if (this.cached) { 180 | urlProvider = new GuavaCachedJwkProvider(urlProvider, cacheSize, expiresIn); 181 | } 182 | return urlProvider; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/NetworkException.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @SuppressWarnings("WeakerAccess") 4 | public class NetworkException extends SigningKeyNotFoundException { 5 | 6 | public NetworkException(String message, Throwable cause) { 7 | super(message, cause); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/RateLimitReachedException.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @SuppressWarnings("WeakerAccess") 4 | public class RateLimitReachedException extends JwkException { 5 | private final long availableInMs; 6 | 7 | public RateLimitReachedException(long availableInMs) { 8 | super(String.format("The Rate Limit has been reached! Please wait %d milliseconds.", availableInMs)); 9 | this.availableInMs = availableInMs; 10 | } 11 | 12 | /** 13 | * Returns the delay in which the jwk request can be retried. 14 | * 15 | * @return the time to wait in milliseconds before retrying the request. 16 | */ 17 | public long getAvailableIn() { 18 | return availableInMs; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/RateLimitedJwkProvider.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | /** 4 | * Jwk provider that limits the amount of Jwks to deliver in a given rate. 5 | */ 6 | @SuppressWarnings("WeakerAccess") 7 | public class RateLimitedJwkProvider implements JwkProvider { 8 | 9 | private final JwkProvider provider; 10 | private final Bucket bucket; 11 | 12 | /** 13 | * Creates a new provider that will check the given Bucket if a jwks can be provided now. 14 | * 15 | * @param bucket bucket to limit the amount of jwk requested in a given amount of time. 16 | * @param provider provider to use to request jwk when the bucket allows it. 17 | */ 18 | public RateLimitedJwkProvider(JwkProvider provider, Bucket bucket) { 19 | this.provider = provider; 20 | this.bucket = bucket; 21 | } 22 | 23 | @Override 24 | public Jwk get(final String keyId) throws JwkException { 25 | if (!bucket.consume()) { 26 | throw new RateLimitReachedException(bucket.willLeakIn()); 27 | } 28 | return provider.get(keyId); 29 | } 30 | 31 | @VisibleForTesting 32 | JwkProvider getBaseProvider() { 33 | return provider; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/SigningKeyNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @SuppressWarnings("WeakerAccess") 4 | public class SigningKeyNotFoundException extends JwkException { 5 | 6 | public SigningKeyNotFoundException(String message, Throwable cause) { 7 | super(message, cause); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/UrlJwkProvider.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.ObjectReader; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.net.*; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * Jwk provider that loads them from a {@link URL} 16 | */ 17 | @SuppressWarnings("WeakerAccess") 18 | public class UrlJwkProvider implements JwkProvider { 19 | 20 | @VisibleForTesting 21 | static final String WELL_KNOWN_JWKS_PATH = "/.well-known/jwks.json"; 22 | 23 | final URL url; 24 | final Proxy proxy; 25 | final Map headers; 26 | final Integer connectTimeout; 27 | final Integer readTimeout; 28 | 29 | private final ObjectReader reader; 30 | 31 | /** 32 | * Creates a provider that loads from the given URL 33 | * 34 | * @param url to load the jwks 35 | */ 36 | public UrlJwkProvider(URL url) { 37 | this(url, null, null, null, null); 38 | } 39 | 40 | /** 41 | * Creates a provider that loads from the given URL using a specified proxy server 42 | * 43 | * @param url to load the jwks 44 | * @param connectTimeout connection timeout in milliseconds (null for default) 45 | * @param readTimeout read timeout in milliseconds (null for default) 46 | * @param proxy proxy server to use when making the connection 47 | */ 48 | public UrlJwkProvider(URL url, Integer connectTimeout, Integer readTimeout, Proxy proxy) { 49 | this(url, connectTimeout, readTimeout, proxy, null); 50 | } 51 | 52 | /** 53 | * Creates a provider that loads from the given URL using custom request headers. 54 | * 55 | * @param url to load the jwks 56 | * @param connectTimeout connection timeout in milliseconds (default is null) 57 | * @param readTimeout read timeout in milliseconds (default is null) 58 | * @param proxy proxy server to use when making the connection (default is null) 59 | * @param headers a map of request header keys to values to send on the request. Default is "Accept: application/json". 60 | */ 61 | public UrlJwkProvider(URL url, Integer connectTimeout, Integer readTimeout, Proxy proxy, Map headers) { 62 | Util.checkArgument(url != null, "A non-null url is required"); 63 | Util.checkArgument(connectTimeout == null || connectTimeout >= 0, "Invalid connect timeout value '" + connectTimeout + "'. Must be a non-negative integer."); 64 | Util.checkArgument(readTimeout == null || readTimeout >= 0, "Invalid read timeout value '" + readTimeout + "'. Must be a non-negative integer."); 65 | 66 | this.url = url; 67 | this.proxy = proxy; 68 | this.connectTimeout = connectTimeout; 69 | this.readTimeout = readTimeout; 70 | this.reader = new ObjectMapper().readerFor(Map.class); 71 | 72 | this.headers = (headers == null) ? 73 | Collections.singletonMap("Accept", "application/json") : headers; 74 | } 75 | 76 | /** 77 | * Creates a provider that loads from the given URL 78 | * 79 | * @param url to load the jwks 80 | * @param connectTimeout connection timeout in milliseconds (null for default) 81 | * @param readTimeout read timeout in milliseconds (null for default) 82 | */ 83 | public UrlJwkProvider(URL url, Integer connectTimeout, Integer readTimeout) { 84 | this(url, connectTimeout, readTimeout, null, null); 85 | } 86 | 87 | /** 88 | * Creates a provider that loads from the given domain's well-known directory. 89 | *

It can be a url link 'https://samples.auth0.com' or just a domain 'samples.auth0.com'. 90 | * If the protocol (http or https) is not provided then https is used by default. 91 | * The default jwks path "/.well-known/jwks.json" is appended to the given string domain. 92 | * If the domain url contains a path, e.g. 'https://auth.example.com/some-resource', the path is preserved and the 93 | * default jwks path is appended. 94 | *

For example, when the domain is "samples.auth0.com" 95 | * the jwks url that will be used is "https://samples.auth0.com/.well-known/jwks.json" 96 | * If the domain string is "https://auth.example.com/some-resource", the jwks url that will be used is 97 | * "https://auth.example.com/some-resource/.well-known/jwks.json" 98 | *

Use {@link #UrlJwkProvider(URL)} if you need to pass a full URL. 99 | * 100 | * @param domain where jwks is published 101 | */ 102 | public UrlJwkProvider(String domain) { 103 | this(urlForDomain(domain)); 104 | } 105 | 106 | static URL urlForDomain(String domain) { 107 | Util.checkArgument(!Util.isNullOrEmpty(domain), "A domain is required"); 108 | 109 | if (!domain.startsWith("http")) { 110 | domain = "https://" + domain; 111 | } 112 | 113 | try { 114 | final URI uri = new URI(domain + WELL_KNOWN_JWKS_PATH).normalize(); 115 | return uri.toURL(); 116 | } catch (MalformedURLException | URISyntaxException e) { 117 | throw new IllegalArgumentException("Invalid jwks uri", e); 118 | } 119 | } 120 | 121 | private Map getJwks() throws SigningKeyNotFoundException { 122 | try { 123 | final URLConnection c = (proxy == null) ? this.url.openConnection() : this.url.openConnection(proxy); 124 | if (connectTimeout != null) { 125 | c.setConnectTimeout(connectTimeout); 126 | } 127 | if (readTimeout != null) { 128 | c.setReadTimeout(readTimeout); 129 | } 130 | 131 | for (Map.Entry entry : headers.entrySet()) { 132 | c.setRequestProperty(entry.getKey(), entry.getValue()); 133 | } 134 | 135 | try (InputStream inputStream = c.getInputStream()) { 136 | return reader.readValue(inputStream); 137 | } 138 | } catch (IOException e) { 139 | throw new NetworkException("Cannot obtain jwks from url " + url.toString(), e); 140 | } 141 | } 142 | 143 | public List getAll() throws SigningKeyNotFoundException { 144 | List jwks = new ArrayList<>(); 145 | @SuppressWarnings("unchecked") final List> keys = (List>) getJwks().get("keys"); 146 | 147 | if (keys == null || keys.isEmpty()) { 148 | throw new SigningKeyNotFoundException("No keys found in " + url.toString(), null); 149 | } 150 | 151 | try { 152 | for (Map values : keys) { 153 | jwks.add(Jwk.fromValues(values)); 154 | } 155 | } catch (IllegalArgumentException e) { 156 | throw new SigningKeyNotFoundException("Failed to parse jwk from json", e); 157 | } 158 | return jwks; 159 | } 160 | 161 | @Override 162 | public Jwk get(String keyId) throws JwkException { 163 | final List jwks = getAll(); 164 | if (keyId == null && jwks.size() == 1) { 165 | return jwks.get(0); 166 | } 167 | if (keyId != null) { 168 | for (Jwk jwk : jwks) { 169 | if (keyId.equals(jwk.getId())) { 170 | return jwk; 171 | } 172 | } 173 | } 174 | throw new SigningKeyNotFoundException("No key found in " + url.toString() + " with kid " + keyId, null); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/Util.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | class Util { 4 | static boolean isNullOrEmpty(String s) { 5 | return s == null || s.isEmpty(); 6 | } 7 | 8 | static void checkArgument(boolean arg, String message) { 9 | if (!arg) { 10 | throw new IllegalArgumentException(String.valueOf(message)); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/auth0/jwk/VisibleForTesting.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | @interface VisibleForTesting { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/BucketImplTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import static org.hamcrest.Matchers.*; 10 | import static org.junit.Assert.assertThat; 11 | 12 | public class BucketImplTest { 13 | @Rule 14 | public ExpectedException exception = ExpectedException.none(); 15 | 16 | private static final long RATE = 500L; 17 | private static final long SIZE = 5L; 18 | 19 | @Test 20 | public void shouldThrowOnCreateWithNegativeSize() throws Exception { 21 | exception.expect(IllegalArgumentException.class); 22 | exception.expectMessage("Invalid bucket size."); 23 | new BucketImpl(-1, 10, TimeUnit.SECONDS); 24 | } 25 | 26 | @Test 27 | public void shouldThrowOnCreateWithNegativeRate() throws Exception { 28 | exception.expect(IllegalArgumentException.class); 29 | exception.expectMessage("Invalid bucket refill rate."); 30 | new BucketImpl(10, -1, TimeUnit.SECONDS); 31 | } 32 | 33 | @Test 34 | public void shouldThrowWhenLeakingMoreThanBucketSize() throws Exception { 35 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.SECONDS); 36 | exception.expect(IllegalArgumentException.class); 37 | exception.expectMessage(String.format("Cannot consume %d tokens when the BucketImpl size is %d!", SIZE + 1, SIZE)); 38 | bucket.willLeakIn(SIZE + 1); 39 | } 40 | 41 | @Test 42 | public void shouldThrowWhenConsumingMoreThanBucketSize() throws Exception { 43 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.SECONDS); 44 | exception.expect(IllegalArgumentException.class); 45 | exception.expectMessage(String.format("Cannot consume %d tokens when the BucketImpl size is %d!", SIZE + 1, SIZE)); 46 | bucket.consume(SIZE + 1); 47 | } 48 | 49 | @Test 50 | public void shouldCreateFullBucket() throws Exception { 51 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 52 | assertThat(bucket, notNullValue()); 53 | assertThat(bucket.willLeakIn(SIZE), equalTo(0L)); 54 | assertThat(bucket.willLeakIn(), equalTo(0L)); 55 | } 56 | 57 | @Test 58 | public void shouldAddOneTokenPerRate() throws Exception { 59 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 60 | assertThat(bucket, notNullValue()); 61 | assertThat(bucket.consume(SIZE), equalTo(true)); 62 | assertThat(bucket.consume(), equalTo(false)); 63 | 64 | //wait for 1 token and consume it 65 | assertThat(bucket.willLeakIn(), allOf(greaterThan(0L), lessThanOrEqualTo(RATE))); 66 | pause(RATE); 67 | assertThat(bucket.consume(), equalTo(true)); 68 | 69 | //wait for 5 tokens and consume them 70 | assertThat(bucket.willLeakIn(SIZE), allOf(greaterThan((SIZE - 1) * RATE), lessThanOrEqualTo(SIZE * RATE))); 71 | pause(SIZE * RATE); 72 | assertThat(bucket.consume(SIZE), equalTo(true)); 73 | 74 | //expect to wait 1 token rate 75 | assertThat(bucket.willLeakIn(), allOf(greaterThan(0L), lessThanOrEqualTo(RATE))); 76 | assertThat(bucket.consume(), equalTo(false)); 77 | } 78 | 79 | @Test 80 | public void shouldNotAddMoreTokensThatTheBucketSize() throws Exception { 81 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 82 | assertThat(bucket, notNullValue()); 83 | assertThat(bucket.willLeakIn(SIZE), equalTo(0L)); 84 | 85 | //Give some time to fill the already full bucket 86 | pause(SIZE * RATE * 2); 87 | assertThat(bucket.consume(SIZE), equalTo(true)); 88 | assertThat(bucket.consume(), equalTo(false)); 89 | } 90 | 91 | @Test 92 | public void shouldConsumeAllBucketTokens() throws Exception { 93 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 94 | assertThat(bucket, notNullValue()); 95 | assertThat(bucket.consume(SIZE), equalTo(true)); 96 | assertThat(bucket.consume(), equalTo(false)); 97 | } 98 | 99 | @Test 100 | public void shouldConsumeByOneToken() throws Exception { 101 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 102 | assertThat(bucket, notNullValue()); 103 | //Consume 5 tokens 104 | assertThat(bucket.consume(), equalTo(true)); 105 | assertThat(bucket.consume(), equalTo(true)); 106 | assertThat(bucket.consume(), equalTo(true)); 107 | assertThat(bucket.consume(), equalTo(true)); 108 | assertThat(bucket.consume(), equalTo(true)); 109 | //should not consume a 6th token 110 | assertThat(bucket.consume(), equalTo(false)); 111 | } 112 | 113 | @Test 114 | public void shouldCalculateRemainingLeakTimeForOneToken() throws Exception { 115 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 116 | assertThat(bucket, notNullValue()); 117 | //Consume 5 tokens 118 | assertThat(bucket.consume(5), equalTo(true)); 119 | assertThat(bucket.willLeakIn(), allOf(greaterThan(0L), lessThanOrEqualTo(RATE))); 120 | // wait half rate time and check if the wait time is correct 121 | pause(RATE / 2); 122 | assertThat(bucket.willLeakIn(), allOf(greaterThan(0L), lessThanOrEqualTo(RATE / 2))); 123 | } 124 | 125 | @Test 126 | public void shouldCalculateRemainingLeakTimeForManyTokens() throws Exception { 127 | Bucket bucket = new BucketImpl(SIZE, RATE, TimeUnit.MILLISECONDS); 128 | assertThat(bucket, notNullValue()); 129 | //Consume 3 tokens 130 | assertThat(bucket.consume(3), equalTo(true)); 131 | 132 | //Expected to wait 3 * RATE time at most to be able to consume 5 tokens 133 | assertThat(bucket.willLeakIn(5), allOf(greaterThanOrEqualTo(RATE * 2), lessThanOrEqualTo(RATE * 3))); 134 | pause(RATE * 3); 135 | assertThat(bucket.willLeakIn(5), allOf(greaterThanOrEqualTo(0L), lessThanOrEqualTo(RATE))); 136 | } 137 | 138 | 139 | @Test 140 | public void shouldCarryDeltaWhenManyTokensAreRequested() throws Exception { 141 | Bucket bucket = new BucketImpl(5, 1000, TimeUnit.MILLISECONDS); 142 | assertThat(bucket, notNullValue()); 143 | 144 | //Consume all tokens. Expect to wait 5 seconds for refill 145 | assertThat(bucket.consume(5), equalTo(true)); 146 | assertThat(bucket.willLeakIn(5), allOf(greaterThanOrEqualTo(4900L), lessThanOrEqualTo(5000L))); 147 | 148 | //wait 1500ms to have 1 token. 149 | pause(1500); 150 | //Consume 1 and expect to wait 500 + 4000 ms if we want to consume 5 again. 151 | assertThat(bucket.consume(), equalTo(true)); 152 | assertThat(bucket.willLeakIn(5), allOf(greaterThanOrEqualTo(4400L), lessThanOrEqualTo(4500L))); 153 | } 154 | 155 | private void pause(long ms) throws InterruptedException { 156 | System.out.println(String.format("Waiting %d ms..", ms)); 157 | Thread.sleep(ms); 158 | } 159 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/GuavaCachedJwkProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | 11 | import static org.hamcrest.Matchers.equalTo; 12 | import static org.junit.Assert.assertThat; 13 | import static org.mockito.Matchers.anyString; 14 | import static org.mockito.Matchers.eq; 15 | import static org.mockito.Mockito.*; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class GuavaCachedJwkProviderTest { 19 | 20 | private static final String KID = "KID"; 21 | private GuavaCachedJwkProvider provider; 22 | 23 | @Mock 24 | private JwkProvider fallback; 25 | 26 | @Mock 27 | private Jwk jwk; 28 | 29 | @Rule 30 | public ExpectedException expectedException = ExpectedException.none(); 31 | 32 | @Before 33 | public void setUp() { 34 | provider = new GuavaCachedJwkProvider(fallback); 35 | } 36 | 37 | @Test 38 | public void shouldFailToGetSingleWhenNotExists() throws Exception { 39 | expectedException.expect(SigningKeyNotFoundException.class); 40 | when(fallback.get(anyString())).thenThrow(new SigningKeyNotFoundException("TEST!", null)); 41 | provider.get(KID); 42 | } 43 | 44 | @Test 45 | public void shouldThrowSigningKeyNotFoundException() throws Exception { 46 | expectedException.expect(SigningKeyNotFoundException.class); 47 | when(fallback.get(anyString())).thenThrow(new SigningKeyNotFoundException("TEST!", null)); 48 | provider.get(KID); 49 | } 50 | 51 | @Test 52 | public void shouldThrowRateLimitReachedException() throws Exception { 53 | expectedException.expect(RateLimitReachedException.class); 54 | when(fallback.get(anyString())).thenThrow(new RateLimitReachedException(1234)); 55 | provider.get(KID); 56 | } 57 | 58 | @Test 59 | public void shouldThrowNetworkException() throws Exception { 60 | expectedException.expect(NetworkException.class); 61 | when(fallback.get(anyString())).thenThrow(new NetworkException("TEST!", null)); 62 | provider.get(KID); 63 | } 64 | 65 | @Test 66 | public void shouldUseFallbackWhenNotCached() throws Exception { 67 | when(fallback.get(eq(KID))).thenReturn(jwk); 68 | assertThat(provider.get(KID), equalTo(jwk)); 69 | verify(fallback).get(eq(KID)); 70 | } 71 | 72 | @Test 73 | public void shouldUseCachedValue() throws Exception { 74 | when(fallback.get(eq(KID))).thenReturn(jwk).thenThrow(new SigningKeyNotFoundException("TEST!", null)); 75 | provider.get(KID); 76 | assertThat(provider.get(KID), equalTo(jwk)); 77 | verify(fallback, only()).get(KID); 78 | } 79 | 80 | @Test 81 | public void shouldCacheWhenIdMatchesDefaultMissingIdKey() throws Exception { 82 | when(fallback.get(eq(GuavaCachedJwkProvider.NULL_KID_KEY))).thenReturn(jwk); 83 | assertThat(provider.get(GuavaCachedJwkProvider.NULL_KID_KEY), equalTo(jwk)); 84 | verify(fallback).get(eq(GuavaCachedJwkProvider.NULL_KID_KEY)); 85 | 86 | verifyNoMoreInteractions(fallback); 87 | assertThat(provider.get(GuavaCachedJwkProvider.NULL_KID_KEY), equalTo(jwk)); 88 | } 89 | 90 | @Test 91 | public void shouldCacheWhenNullId() throws Exception { 92 | when(fallback.get(eq(null))).thenReturn(jwk); 93 | assertThat(provider.get(null), equalTo(jwk)); 94 | verify(fallback).get(eq(null)); 95 | 96 | verifyNoMoreInteractions(fallback); 97 | assertThat(provider.get(null), equalTo(jwk)); 98 | } 99 | 100 | @Test 101 | public void shouldGetBaseProvider() { 102 | assertThat(provider.getBaseProvider(), equalTo(fallback)); 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/JwkProviderBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Rule; 4 | import org.junit.Test; 5 | import org.junit.rules.ExpectedException; 6 | 7 | import java.net.InetSocketAddress; 8 | import java.net.Proxy; 9 | import java.net.URL; 10 | import java.util.Collections; 11 | import java.util.Map; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import static com.auth0.jwk.UrlJwkProvider.WELL_KNOWN_JWKS_PATH; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.hamcrest.Matchers.*; 17 | 18 | public class JwkProviderBuilderTest { 19 | 20 | @Rule 21 | public ExpectedException expectedException = ExpectedException.none(); 22 | 23 | private String domain = "samples.auth0.com"; 24 | private String normalizedDomain = "https://" + domain; 25 | 26 | @Test 27 | public void shouldCreateForUrl() throws Exception { 28 | URL urlToJwks = new URL(normalizedDomain + WELL_KNOWN_JWKS_PATH); 29 | assertThat(new JwkProviderBuilder(urlToJwks).build(), notNullValue()); 30 | } 31 | 32 | @Test 33 | public void shouldCreateForDomain() { 34 | assertThat(new JwkProviderBuilder(domain).build(), notNullValue()); 35 | } 36 | 37 | @Test 38 | public void shouldCreateForNormalizedDomain() { 39 | assertThat(new JwkProviderBuilder(normalizedDomain).build(), notNullValue()); 40 | } 41 | 42 | @Test 43 | public void shouldFailWhenNoUrlIsProvided() { 44 | expectedException.expect(IllegalStateException.class); 45 | expectedException.expectMessage("Cannot build provider without url to jwks"); 46 | new JwkProviderBuilder((URL) null).build(); 47 | } 48 | 49 | @Test 50 | public void shouldFailWhenNoDomainIsProvided() { 51 | expectedException.expect(IllegalStateException.class); 52 | expectedException.expectMessage("Cannot build provider without domain"); 53 | new JwkProviderBuilder((String) null).build(); 54 | } 55 | 56 | @Test 57 | public void shouldCreateCachedProvider() { 58 | JwkProvider provider = new JwkProviderBuilder(domain) 59 | .rateLimited(false) 60 | .cached(true) 61 | .build(); 62 | assertThat(provider, notNullValue()); 63 | assertThat(provider, instanceOf(GuavaCachedJwkProvider.class)); 64 | assertThat(((GuavaCachedJwkProvider) provider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 65 | } 66 | 67 | @Test 68 | public void shouldCreateCachedProviderWithCustomValues() { 69 | JwkProvider provider = new JwkProviderBuilder(domain) 70 | .rateLimited(false) 71 | .cached(10, 24, TimeUnit.HOURS) 72 | .build(); 73 | assertThat(provider, notNullValue()); 74 | assertThat(provider, instanceOf(GuavaCachedJwkProvider.class)); 75 | assertThat(((GuavaCachedJwkProvider) provider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 76 | } 77 | 78 | @Test 79 | public void shouldCreateRateLimitedProvider() { 80 | JwkProvider provider = new JwkProviderBuilder(domain) 81 | .cached(false) 82 | .rateLimited(true) 83 | .build(); 84 | assertThat(provider, notNullValue()); 85 | assertThat(provider, instanceOf(RateLimitedJwkProvider.class)); 86 | assertThat(((RateLimitedJwkProvider) provider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 87 | } 88 | 89 | @Test 90 | public void shouldCreateRateLimitedProviderWithCustomValues() { 91 | JwkProvider provider = new JwkProviderBuilder(domain) 92 | .cached(false) 93 | .rateLimited(10, 24, TimeUnit.HOURS) 94 | .build(); 95 | assertThat(provider, notNullValue()); 96 | assertThat(provider, instanceOf(RateLimitedJwkProvider.class)); 97 | assertThat(((RateLimitedJwkProvider) provider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 98 | } 99 | 100 | @Test 101 | public void shouldCreateCachedAndRateLimitedProvider() { 102 | JwkProvider provider = new JwkProviderBuilder(domain) 103 | .cached(true) 104 | .rateLimited(true) 105 | .build(); 106 | assertThat(provider, notNullValue()); 107 | assertThat(provider, instanceOf(GuavaCachedJwkProvider.class)); 108 | JwkProvider baseProvider = ((GuavaCachedJwkProvider) provider).getBaseProvider(); 109 | assertThat(baseProvider, instanceOf(RateLimitedJwkProvider.class)); 110 | assertThat(((RateLimitedJwkProvider) baseProvider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 111 | } 112 | 113 | @Test 114 | public void shouldCreateCachedAndRateLimitedProviderWithCustomValues() { 115 | JwkProvider provider = new JwkProviderBuilder(domain) 116 | .cached(10, 24, TimeUnit.HOURS) 117 | .rateLimited(10, 24, TimeUnit.HOURS) 118 | .build(); 119 | assertThat(provider, notNullValue()); 120 | assertThat(provider, instanceOf(GuavaCachedJwkProvider.class)); 121 | JwkProvider baseProvider = ((GuavaCachedJwkProvider) provider).getBaseProvider(); 122 | assertThat(baseProvider, instanceOf(RateLimitedJwkProvider.class)); 123 | assertThat(((RateLimitedJwkProvider) baseProvider).getBaseProvider(), instanceOf(UrlJwkProvider.class)); 124 | } 125 | 126 | @Test 127 | public void shouldCreateCachedAndRateLimitedProviderByDefault() { 128 | JwkProvider provider = new JwkProviderBuilder(domain).build(); 129 | assertThat(provider, notNullValue()); 130 | assertThat(provider, instanceOf(GuavaCachedJwkProvider.class)); 131 | 132 | JwkProvider wrappedCachedProvider = ((GuavaCachedJwkProvider) provider).getBaseProvider(); 133 | assertThat(wrappedCachedProvider, instanceOf(RateLimitedJwkProvider.class)); 134 | 135 | JwkProvider wrappedRateLimitedProvider = ((RateLimitedJwkProvider) wrappedCachedProvider).getBaseProvider(); 136 | assertThat(wrappedRateLimitedProvider, instanceOf(UrlJwkProvider.class)); 137 | 138 | UrlJwkProvider wrappedUrlProvider = ((UrlJwkProvider) wrappedRateLimitedProvider); 139 | assertThat(wrappedUrlProvider.connectTimeout, is(nullValue())); 140 | assertThat(wrappedUrlProvider.readTimeout, is(nullValue())); 141 | } 142 | 143 | @Test 144 | public void shouldSupportUrlToJwksDomainWithSubPath() throws Exception { 145 | String urlToJwksWithSubPath = normalizedDomain + "/sub/path" + WELL_KNOWN_JWKS_PATH; 146 | URL url = new URL(urlToJwksWithSubPath); 147 | JwkProvider provider = new JwkProviderBuilder(url) 148 | .rateLimited(false) 149 | .cached(false) 150 | .build(); 151 | assertThat(provider, notNullValue()); 152 | assertThat(provider, instanceOf(UrlJwkProvider.class)); 153 | UrlJwkProvider urlJwkProvider = (UrlJwkProvider) provider; 154 | assertThat(urlJwkProvider.url.toString(), equalTo(urlToJwksWithSubPath)); 155 | } 156 | 157 | @Test 158 | public void shouldCreateForUrlAndProxy() throws Exception { 159 | URL url = new URL(normalizedDomain + WELL_KNOWN_JWKS_PATH); 160 | Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.hostname", 8080)); 161 | JwkProvider provider = new JwkProviderBuilder(url) 162 | .proxied(proxy) 163 | .rateLimited(false) 164 | .cached(false) 165 | .build(); 166 | assertThat(provider, notNullValue()); 167 | UrlJwkProvider urlJwkProvider = (UrlJwkProvider) provider; 168 | assertThat(urlJwkProvider.proxy, equalTo(proxy)); 169 | } 170 | 171 | @Test 172 | public void shouldCreateForUrlAndTimeouts() throws Exception { 173 | URL url = new URL(normalizedDomain + WELL_KNOWN_JWKS_PATH); 174 | JwkProvider provider = new JwkProviderBuilder(url) 175 | .timeouts(9999, 1111) 176 | .rateLimited(false) 177 | .cached(false) 178 | .build(); 179 | assertThat(provider, notNullValue()); 180 | UrlJwkProvider urlJwkProvider = (UrlJwkProvider) provider; 181 | assertThat(urlJwkProvider.connectTimeout, equalTo(9999)); 182 | assertThat(urlJwkProvider.readTimeout, equalTo(1111)); 183 | } 184 | 185 | @Test 186 | public void shouldCreateForUrlWithCustomHeaders() throws Exception { 187 | URL url = new URL(normalizedDomain + WELL_KNOWN_JWKS_PATH); 188 | Map headers = Collections.singletonMap("header", "value"); 189 | JwkProvider provider = new JwkProviderBuilder(url) 190 | .rateLimited(false) 191 | .cached(false) 192 | .headers(headers) 193 | .build(); 194 | assertThat(provider, notNullValue()); 195 | UrlJwkProvider urlJwkProvider = (UrlJwkProvider) provider; 196 | assertThat(urlJwkProvider.headers, equalTo(headers)); 197 | } 198 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/JwkTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.common.collect.Maps; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.rules.ExpectedException; 8 | 9 | import java.security.SecureRandom; 10 | import java.security.interfaces.ECPublicKey; 11 | import java.security.interfaces.RSAPublicKey; 12 | import java.util.Base64; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | import static org.hamcrest.Matchers.*; 17 | import static org.junit.Assert.assertThat; 18 | 19 | public class JwkTest { 20 | 21 | private static final String RS_256 = "RS256"; 22 | private static final String RSA = "RSA"; 23 | 24 | private static final String ES_256 = "ES256"; 25 | private static final String ES_384 = "ES384"; 26 | private static final String ES_521 = "ES521"; 27 | private static final String ES_UNSUPPORTED = "ES999"; 28 | private static final String EC = "EC"; 29 | private static final String CRV_P_256 = "P-256"; 30 | private static final String CRV_P_384 = "P-384"; 31 | private static final String CRV_P_521 = "P-521"; 32 | private static final String CRV_UNSUPPORTED = "P-111"; 33 | private static final String EC_P_256_X = "3xlexeXJEVNEJIRZKLJxjksjerixlwqEIEI586ss__3"; 34 | private static final String EC_P_256_Y = "398jKSKSxnbnbmvirt794798cvijaoowrvjvjeiKSLS"; 35 | private static final String EC_P_384_X = "3xlexeXJEVNEJIRZKLJxjksjerixlwqEIEI586ss__32929lxkXKWWI1___221XX"; 36 | private static final String EC_P_384_Y = "398jKSKSxnbnbmvirt794798cvijaoowrvjvjeiKSLSkdksl383838zzze342246"; 37 | private static final String EC_P_521_X = "3xlexeXJEVNEJIRZKLJxjksjerixlwqEIEI586ss__32929lxkXKWWI1___221XXx39392xXetixeiazqwernvbc"; 38 | private static final String EC_P_521_Y = "398jKSKSxnbnbmvirt794798cvijaoowrvjvjeiKSLSkdksl383838zzze342246_349809iubjairlKDSLDLKxx"; 39 | 40 | private static final String AES = "AES"; 41 | private static final String SIG = "sig"; 42 | private static final String THUMBPRINT = "THUMBPRINT"; 43 | private static final String MODULUS = "vGChUGMTWZNfRsXxd-BtzC4RDYOMqtIhWHol--HNib5SgudWBg6rEcxvR6LWrx57N6vfo68wwT9_FHlZpaK6NXA_dWFW4f3NftfWLL7Bqy90sO4vijM6LMSE6rnl5VB9_Gsynk7_jyTgYWdTwKur0YRec93eha9oCEXmy7Ob1I2dJ8OQmv2GlvA7XZalMxAq4rFnXLzNQ7hCsHrUJP1p7_7SolWm9vTokkmckzSI_mAH2R27Z56DmI7jUkL9fLU-jz-fz4bkNg-mPz4R-kUmM_ld3-xvto79BtxJvOw5qqtLNnRjiDzoqRv-WrBdw5Vj8Pvrg1fwscfVWHlmq-1pFQ"; 44 | private static final String EXPONENT = "AQAB"; 45 | private static final String CERT_CHAIN = "CERT_CHAIN"; 46 | private static final List KEY_OPS_LIST = Lists.newArrayList("sign"); 47 | private static final String KEY_OPS_STRING = "sign"; 48 | 49 | @Rule 50 | public ExpectedException expectedException = ExpectedException.none(); 51 | 52 | @Test 53 | public void shouldBuildRsaWithMap() throws Exception { 54 | final String kid = randomKeyId(); 55 | Map values = publicKeyRsaValues(kid, KEY_OPS_LIST); 56 | Jwk jwk = Jwk.fromValues(values); 57 | 58 | assertThat(jwk.getId(), equalTo(kid)); 59 | assertThat(jwk.getAlgorithm(), equalTo(RS_256)); 60 | assertThat(jwk.getType(), equalTo(RSA)); 61 | assertThat(jwk.getUsage(), equalTo(SIG)); 62 | assertThat(jwk.getOperationsAsList(), equalTo(KEY_OPS_LIST)); 63 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 64 | assertThat(jwk.getCertificateThumbprint(), equalTo(THUMBPRINT)); 65 | assertThat(jwk.getCertificateChain(), contains(CERT_CHAIN)); 66 | } 67 | 68 | @Test 69 | public void shouldBuildEllipticCurveWithMap() throws Exception { 70 | final String kid = randomKeyId(); 71 | Map values = publicKeyEllipticCurveValues(kid, ES_256, KEY_OPS_LIST, CRV_P_256, EC_P_256_X, EC_P_256_Y); 72 | Jwk jwk = Jwk.fromValues(values); 73 | 74 | assertThat(jwk.getId(), equalTo(kid)); 75 | assertThat(jwk.getAlgorithm(), equalTo(ES_256)); 76 | assertThat(jwk.getType(), equalTo(EC)); 77 | assertThat(jwk.getUsage(), equalTo(SIG)); 78 | assertThat(jwk.getOperationsAsList(), equalTo(KEY_OPS_LIST)); 79 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 80 | assertThat(jwk.getCertificateThumbprint(), nullValue()); 81 | assertThat(jwk.getCertificateChain(), nullValue()); 82 | } 83 | 84 | @Test 85 | public void shouldReturnRsaPublicKey() throws Exception { 86 | final String kid = randomKeyId(); 87 | Map values = publicKeyRsaValues(kid, KEY_OPS_LIST); 88 | Jwk jwk = Jwk.fromValues(values); 89 | 90 | assertThat(jwk.getPublicKey(), notNullValue()); 91 | assertThat(jwk.getPublicKey(), instanceOf(RSAPublicKey.class)); 92 | assertThat(jwk.getOperationsAsList(), is(KEY_OPS_LIST)); 93 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 94 | } 95 | 96 | @Test 97 | public void shouldReturnEllipticCurveP256PublicKey() throws Exception { 98 | final String kid = randomKeyId(); 99 | Map values = publicKeyEllipticCurveValues(kid, ES_256, KEY_OPS_LIST, CRV_P_256, EC_P_256_X, EC_P_256_Y); 100 | Jwk jwk = Jwk.fromValues(values); 101 | 102 | assertThat(jwk.getPublicKey(), notNullValue()); 103 | assertThat(jwk.getPublicKey(), instanceOf(ECPublicKey.class)); 104 | assertThat(jwk.getOperationsAsList(), is(KEY_OPS_LIST)); 105 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 106 | } 107 | 108 | @Test 109 | public void shouldReturnEllipticCurveP384PublicKey() throws Exception { 110 | final String kid = randomKeyId(); 111 | Map values = publicKeyEllipticCurveValues(kid, ES_384, KEY_OPS_LIST, CRV_P_384, EC_P_384_X, EC_P_384_Y); 112 | Jwk jwk = Jwk.fromValues(values); 113 | 114 | assertThat(jwk.getPublicKey(), notNullValue()); 115 | assertThat(jwk.getPublicKey(), instanceOf(ECPublicKey.class)); 116 | assertThat(jwk.getOperationsAsList(), is(KEY_OPS_LIST)); 117 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 118 | } 119 | 120 | @Test 121 | public void shouldReturnEllipticCurveP521PublicKey() throws Exception { 122 | final String kid = randomKeyId(); 123 | Map values = publicKeyEllipticCurveValues(kid, ES_521, KEY_OPS_LIST, CRV_P_521, EC_P_521_X, EC_P_521_Y); 124 | Jwk jwk = Jwk.fromValues(values); 125 | 126 | assertThat(jwk.getPublicKey(), notNullValue()); 127 | assertThat(jwk.getPublicKey(), instanceOf(ECPublicKey.class)); 128 | assertThat(jwk.getOperationsAsList(), is(KEY_OPS_LIST)); 129 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 130 | } 131 | 132 | @Test 133 | public void shouldThrowForUnsupportedEllipticCurvePublicKey() throws Exception { 134 | final String kid = randomKeyId(); 135 | Map values = publicKeyEllipticCurveValues(kid, ES_UNSUPPORTED, KEY_OPS_LIST, CRV_UNSUPPORTED, EC_P_521_X, EC_P_521_Y); 136 | Jwk jwk = Jwk.fromValues(values); 137 | expectedException.expect(InvalidPublicKeyException.class); 138 | expectedException.expectMessage("Invalid or unsupported curve type " + CRV_UNSUPPORTED); 139 | jwk.getPublicKey(); 140 | } 141 | 142 | @Test 143 | public void shouldReturnPublicKeyForStringKeyOpsParam() throws Exception { 144 | final String kid = randomKeyId(); 145 | Map values = publicKeyRsaValues(kid, KEY_OPS_STRING); 146 | Jwk jwk = Jwk.fromValues(values); 147 | 148 | assertThat(jwk.getPublicKey(), notNullValue()); 149 | assertThat(jwk.getOperationsAsList(), is(KEY_OPS_LIST)); 150 | assertThat(jwk.getOperations(), is(KEY_OPS_STRING)); 151 | } 152 | 153 | @Test 154 | public void shouldReturnPublicKeyForNullKeyOpsParam() throws Exception { 155 | final String kid = randomKeyId(); 156 | Map values = publicKeyRsaValues(kid, null); 157 | Jwk jwk = Jwk.fromValues(values); 158 | 159 | assertThat(jwk.getPublicKey(), notNullValue()); 160 | assertThat(jwk.getOperationsAsList(), nullValue()); 161 | assertThat(jwk.getOperations(), nullValue()); 162 | } 163 | 164 | @Test 165 | public void shouldReturnPublicKeyForEmptyKeyOpsParam() throws Exception { 166 | final String kid = randomKeyId(); 167 | Map values = publicKeyRsaValues(kid, Lists.newArrayList()); 168 | Jwk jwk = Jwk.fromValues(values); 169 | 170 | assertThat(jwk.getPublicKey(), notNullValue()); 171 | assertThat(jwk.getOperationsAsList(), notNullValue()); 172 | assertThat(jwk.getOperationsAsList().size(), equalTo(0)); 173 | assertThat(jwk.getOperations(), nullValue()); 174 | } 175 | 176 | @Test 177 | public void shouldThrowForUnsupportedKeyType() throws Exception { 178 | final String kid = randomKeyId(); 179 | Map values = unsupportedValues(kid); 180 | Jwk jwk = Jwk.fromValues(values); 181 | expectedException.expect(InvalidPublicKeyException.class); 182 | expectedException.expectMessage("The key type of " + AES + " is not supported"); 183 | jwk.getPublicKey(); 184 | } 185 | 186 | @Test 187 | public void shouldNotThrowInvalidArgumentExceptionOnMissingKidParam() throws Exception { 188 | //kid is optional - https://tools.ietf.org/html/rfc7517#section-4.5 189 | final String kid = randomKeyId(); 190 | Map values = publicKeyRsaValues(kid, KEY_OPS_LIST); 191 | values.remove("kid"); 192 | Jwk.fromValues(values); 193 | } 194 | 195 | @Test 196 | public void shouldThrowInvalidArgumentExceptionOnMissingKtyParam() throws Exception { 197 | final String kid = randomKeyId(); 198 | Map values = publicKeyRsaValues(kid, KEY_OPS_LIST); 199 | values.remove("kty"); 200 | expectedException.expect(IllegalArgumentException.class); 201 | Jwk.fromValues(values); 202 | } 203 | 204 | @Test 205 | public void shouldReturnKeyWithMissingAlgParam() throws Exception { 206 | final String kid = randomKeyId(); 207 | Map values = publicKeyRsaValues(kid, KEY_OPS_LIST); 208 | values.remove("alg"); 209 | Jwk jwk = Jwk.fromValues(values); 210 | assertThat(jwk.getPublicKey(), notNullValue()); 211 | } 212 | 213 | private static String randomKeyId() { 214 | byte[] bytes = new byte[50]; 215 | new SecureRandom().nextBytes(bytes); 216 | return Base64.getEncoder().encodeToString(bytes); 217 | } 218 | 219 | private static Map unsupportedValues(String kid) { 220 | Map values = Maps.newHashMap(); 221 | values.put("alg", "AES_256"); 222 | values.put("kty", AES); 223 | values.put("use", SIG); 224 | values.put("kid", kid); 225 | return values; 226 | } 227 | 228 | private static Map publicKeyRsaValues(String kid, Object keyOps) { 229 | Map values = Maps.newHashMap(); 230 | values.put("alg", RS_256); 231 | values.put("kty", RSA); 232 | values.put("use", SIG); 233 | values.put("key_ops", keyOps); 234 | values.put("x5c", Lists.newArrayList(CERT_CHAIN)); 235 | values.put("x5t", THUMBPRINT); 236 | values.put("kid", kid); 237 | values.put("n", MODULUS); 238 | values.put("e", EXPONENT); 239 | return values; 240 | } 241 | 242 | private static Map publicKeyEllipticCurveValues(String kid, String alg, Object keyOps, String crv, String x, String y) { 243 | Map values = Maps.newHashMap(); 244 | values.put("alg", alg); 245 | values.put("kty", EC); 246 | values.put("use", SIG); 247 | values.put("key_ops", keyOps); 248 | values.put("kid", kid); 249 | values.put("x", x); 250 | values.put("y", y); 251 | values.put("crv", crv); 252 | return values; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/RateLimitReachedExceptionTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.Matchers.equalTo; 6 | import static org.hamcrest.Matchers.notNullValue; 7 | import static org.junit.Assert.assertThat; 8 | 9 | public class RateLimitReachedExceptionTest { 10 | @Test 11 | public void shouldGetAvailableIn() throws Exception { 12 | RateLimitReachedException exception = new RateLimitReachedException(123456789); 13 | assertThat(exception, notNullValue()); 14 | assertThat(exception.getMessage(), equalTo("The Rate Limit has been reached! Please wait 123456789 milliseconds.")); 15 | assertThat(exception.getAvailableIn(), equalTo(123456789L)); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/RateLimitedJwkProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.runners.MockitoJUnitRunner; 10 | 11 | import static org.hamcrest.Matchers.equalTo; 12 | import static org.junit.Assert.assertThat; 13 | import static org.mockito.Matchers.eq; 14 | import static org.mockito.Mockito.verify; 15 | import static org.mockito.Mockito.when; 16 | 17 | @RunWith(MockitoJUnitRunner.class) 18 | public class RateLimitedJwkProviderTest { 19 | 20 | private static final String KID = "KID"; 21 | private RateLimitedJwkProvider provider; 22 | 23 | @Mock 24 | private JwkProvider fallback; 25 | 26 | @Mock 27 | private Jwk jwk; 28 | 29 | @Mock 30 | private Bucket bucket; 31 | 32 | @Rule 33 | public ExpectedException expectedException = ExpectedException.none(); 34 | 35 | @Before 36 | public void setUp() throws Exception { 37 | provider = new RateLimitedJwkProvider(fallback, bucket); 38 | } 39 | 40 | @Test 41 | public void shouldFailToGetWhenBucketIsEmpty() throws Exception { 42 | when(bucket.consume()).thenReturn(false); 43 | expectedException.expect(RateLimitReachedException.class); 44 | when(fallback.get(eq(KID))).thenReturn(jwk); 45 | provider.get(KID); 46 | } 47 | 48 | @Test 49 | public void shouldGetWhenBucketHasTokensAvailable() throws Exception { 50 | when(bucket.consume()).thenReturn(true); 51 | when(fallback.get(eq(KID))).thenReturn(jwk); 52 | assertThat(provider.get(KID), equalTo(jwk)); 53 | verify(fallback).get(eq(KID)); 54 | } 55 | 56 | @Test 57 | public void shouldGetBaseProvider() throws Exception { 58 | assertThat(provider.getBaseProvider(), equalTo(fallback)); 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/UrlJwkProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Before; 4 | import org.junit.Rule; 5 | import org.junit.Test; 6 | import org.junit.rules.ExpectedException; 7 | import org.mockito.ArgumentCaptor; 8 | import org.mockito.invocation.InvocationOnMock; 9 | import org.mockito.stubbing.Answer; 10 | 11 | import java.io.IOException; 12 | import java.lang.ref.WeakReference; 13 | import java.net.*; 14 | import java.util.Collections; 15 | import java.util.List; 16 | 17 | import static com.auth0.jwk.UrlJwkProvider.WELL_KNOWN_JWKS_PATH; 18 | import static org.hamcrest.Matchers.*; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.junit.Assert.assertThat; 21 | import static org.mockito.Mockito.*; 22 | 23 | public class UrlJwkProviderTest { 24 | 25 | private static final String KID = "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg"; 26 | 27 | @Rule 28 | public ExpectedException expectedException = ExpectedException.none(); 29 | 30 | @Before 31 | public void setUp() { 32 | } 33 | 34 | @Test 35 | public void shouldFailWithNullUrl() { 36 | expectedException.expect(IllegalArgumentException.class); 37 | new UrlJwkProvider((URL) null); 38 | } 39 | 40 | @Test 41 | public void shouldFailToCreateWithNullDomain() { 42 | expectedException.expect(IllegalArgumentException.class); 43 | new UrlJwkProvider((String) null); 44 | } 45 | 46 | @Test 47 | public void shouldFailToCreateWithEmptyDomain() { 48 | expectedException.expect(IllegalArgumentException.class); 49 | new UrlJwkProvider(""); 50 | } 51 | 52 | @Test 53 | public void shouldReturnWithoutIdWhenSingleJwk() throws Exception { 54 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/jwks-single-no-kid.json")); 55 | assertThat(provider.get(null), notNullValue()); 56 | 57 | UrlJwkProvider provider2 = new UrlJwkProvider(getClass().getResource("/jwks-single.json")); 58 | assertThat(provider2.get(null), notNullValue()); 59 | } 60 | 61 | @Test 62 | public void shouldReturnByIdWhenSingleJwk() throws Exception { 63 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/jwks-single.json")); 64 | assertThat(provider.get(KID), notNullValue()); 65 | } 66 | 67 | @Test 68 | public void shouldReturnSingleJwkById() throws Exception { 69 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/jwks.json")); 70 | assertThat(provider.get(KID), notNullValue()); 71 | } 72 | 73 | @Test 74 | public void shouldFailToLoadSingleWithoutIdWhenMultipleJwk() throws Exception { 75 | expectedException.expect(SigningKeyNotFoundException.class); 76 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/jwks.json")); 77 | provider.get(null); 78 | } 79 | 80 | @Test 81 | public void shouldFailToLoadByDifferentIdWhenSingleJwk() throws Exception { 82 | expectedException.expect(SigningKeyNotFoundException.class); 83 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/jwks-single-no-kid.json")); 84 | provider.get("wrong-kid"); 85 | } 86 | 87 | @Test 88 | public void shouldFailToLoadSingleWhenUrlHasNothing() throws Exception { 89 | expectedException.expect(SigningKeyNotFoundException.class); 90 | UrlJwkProvider provider = new UrlJwkProvider(new URL("file:///not_found.file")); 91 | provider.get(KID); 92 | } 93 | 94 | @Test 95 | public void shouldFailToLoadSingleWhenKeysIsEmpty() throws Exception { 96 | expectedException.expect(SigningKeyNotFoundException.class); 97 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/empty-jwks.json")); 98 | provider.get(KID); 99 | } 100 | 101 | @Test 102 | public void shouldFailToLoadSingleWhenJsonIsInvalid() throws Exception { 103 | expectedException.expect(SigningKeyNotFoundException.class); 104 | UrlJwkProvider provider = new UrlJwkProvider(getClass().getResource("/invalid-jwks.json")); 105 | provider.get(KID); 106 | } 107 | 108 | @Test 109 | public void shouldBuildCorrectHttpsUrlOnDomain() { 110 | String domain = "samples.auth0.com"; 111 | String actualJwksUrl = new UrlJwkProvider(domain).url.toString(); 112 | assertThat(actualJwksUrl, equalTo("https://" + domain + WELL_KNOWN_JWKS_PATH)); 113 | } 114 | 115 | @Test 116 | public void shouldWorkOnDomainWithSlash() { 117 | String domain = "samples.auth0.com"; 118 | String domainWithSlash = domain + "/"; 119 | String actualJwksUrl = new UrlJwkProvider(domainWithSlash).url.toString(); 120 | assertThat(actualJwksUrl, equalTo("https://" + domain + WELL_KNOWN_JWKS_PATH)); 121 | } 122 | 123 | @Test 124 | public void shouldBuildCorrectHttpsUrlOnDomainWithHttps() { 125 | String httpsDomain = "https://samples.auth0.com"; 126 | String actualJwksUrl = new UrlJwkProvider(httpsDomain).url.toString(); 127 | assertThat(actualJwksUrl, equalTo(httpsDomain + WELL_KNOWN_JWKS_PATH)); 128 | } 129 | 130 | @Test 131 | public void shouldBuildCorrectHttpsUrlOnDomainWithHttpsAndSlash() { 132 | String httpsDomain = "https://samples.auth0.com"; 133 | String httpsDomainWithSlash = httpsDomain + "/"; 134 | String actualJwksUrl = new UrlJwkProvider(httpsDomainWithSlash).url.toString(); 135 | assertThat(actualJwksUrl, equalTo(httpsDomain + WELL_KNOWN_JWKS_PATH)); 136 | } 137 | 138 | @Test 139 | public void shouldBuildCorrectHttpUrlOnDomainWithHttp() { 140 | String httpDomain = "http://samples.auth0.com"; 141 | String actualJwksUrl = new UrlJwkProvider(httpDomain).url.toString(); 142 | assertThat(actualJwksUrl, equalTo(httpDomain + WELL_KNOWN_JWKS_PATH)); 143 | } 144 | 145 | @Test 146 | public void shouldBuildCorrectHttpUrlOnDomainWithHttpAndSlash() { 147 | String httpDomain = "http://samples.auth0.com"; 148 | String httpDomainWithSlash = httpDomain + "/"; 149 | String actualJwksUrl = new UrlJwkProvider(httpDomainWithSlash).url.toString(); 150 | assertThat(actualJwksUrl, equalTo(httpDomain + WELL_KNOWN_JWKS_PATH)); 151 | } 152 | 153 | @Test 154 | public void shouldUseDomainAndPathWithSlashIfPresent() { 155 | String domain = "samples.auth0.com"; 156 | String domainWithSubPath = domain + "/sub/path/"; 157 | String actualJwksUrl = new UrlJwkProvider(domainWithSubPath).url.toString(); 158 | assertThat(actualJwksUrl, equalTo("https://" + domain + "/sub/path" + WELL_KNOWN_JWKS_PATH)); 159 | } 160 | 161 | @Test 162 | public void shouldUseDomainAndPathWithoutSlashIfPresent() { 163 | String domain = "samples.auth0.com"; 164 | String domainWithSubPath = domain + "/sub/path"; 165 | String actualJwksUrl = new UrlJwkProvider(domainWithSubPath).url.toString(); 166 | assertThat(actualJwksUrl, equalTo("https://" + domain + "/sub/path" + WELL_KNOWN_JWKS_PATH)); 167 | } 168 | 169 | @Test 170 | public void shouldUseDomainAndSinglePathWithSlashIfPresent() { 171 | String domain = "samples.auth0.com"; 172 | String domainWithSubPath = domain + "/path/"; 173 | String actualJwksUrl = new UrlJwkProvider(domainWithSubPath).url.toString(); 174 | assertThat(actualJwksUrl, equalTo("https://" + domain + "/path" + WELL_KNOWN_JWKS_PATH)); 175 | } 176 | 177 | @Test 178 | public void shouldUseDomainAndSinglePathWithoutSlashIfPresent() { 179 | String domain = "samples.auth0.com"; 180 | String domainWithSubPath = domain + "/path"; 181 | String actualJwksUrl = new UrlJwkProvider(domainWithSubPath).url.toString(); 182 | assertThat(actualJwksUrl, equalTo("https://" + domain + "/path" + WELL_KNOWN_JWKS_PATH)); 183 | } 184 | 185 | @Test 186 | public void shouldFailOnInvalidProtocol() { 187 | expectedException.expect(IllegalArgumentException.class); 188 | String domainWithInvalidProtocol = "httptest://samples.auth0.com"; 189 | new UrlJwkProvider(domainWithInvalidProtocol); 190 | } 191 | 192 | @Test 193 | public void shouldFailWithNegativeConnectTimeout() throws MalformedURLException { 194 | expectedException.expect(IllegalArgumentException.class); 195 | new UrlJwkProvider(new URL("https://localhost"), -1, null); 196 | } 197 | 198 | @Test 199 | public void shouldFailWithNegativeReadTimeout() throws MalformedURLException { 200 | expectedException.expect(IllegalArgumentException.class); 201 | new UrlJwkProvider(new URL("https://localhost"), null, -1); 202 | } 203 | 204 | private static class MockURLStreamHandlerFactory implements URLStreamHandlerFactory { 205 | 206 | // The weak reference is just a safeguard against objects not being released 207 | // for garbage collection 208 | private final WeakReference urlConnectionValue; 209 | public WeakReference urlUsed; 210 | public WeakReference proxyUsed; 211 | 212 | public MockURLStreamHandlerFactory(URLConnection urlConnection) { 213 | this.urlConnectionValue = new WeakReference<>(urlConnection); 214 | this.urlUsed = new WeakReference<>(null); 215 | this.proxyUsed = new WeakReference<>(null); 216 | } 217 | 218 | public void clear() { 219 | clearUsed(); 220 | this.urlConnectionValue.clear(); 221 | } 222 | 223 | private void clearUsed() { 224 | this.urlUsed.clear(); 225 | this.proxyUsed.clear(); 226 | } 227 | 228 | private void setUsed(URL u, Proxy p) { 229 | clearUsed(); 230 | urlUsed = new WeakReference<>(u); 231 | proxyUsed = new WeakReference<>(p); 232 | } 233 | 234 | @Override 235 | public URLStreamHandler createURLStreamHandler(String protocol) { 236 | return "mock".equals(protocol) ? new URLStreamHandler() { 237 | @Override 238 | protected URLConnection openConnection(URL u, Proxy p) throws IOException { 239 | setUsed(u, p); 240 | return urlConnectionValue.get(); 241 | } 242 | 243 | @Override 244 | protected URLConnection openConnection(URL u) throws IOException { 245 | setUsed(u, null); 246 | return urlConnectionValue.get(); 247 | } 248 | } : null; 249 | } 250 | } 251 | 252 | @Test 253 | public void shouldConfigureURLConnection() throws Exception { 254 | URLConnection urlConnection = mock(URLConnection.class); 255 | when(urlConnection.getInputStream()).thenAnswer(new Answer() { 256 | @Override 257 | public Object answer(InvocationOnMock invocation) throws Throwable { 258 | return getClass().getResourceAsStream("/jwks.json"); 259 | } 260 | }); 261 | 262 | // Although somewhat of a hack, this approach gets the job done - this method can 263 | // only be called once per virtual machine, but that is sufficient for now. 264 | MockURLStreamHandlerFactory mockFactory = new MockURLStreamHandlerFactory(urlConnection); 265 | URL.setURLStreamHandlerFactory(mockFactory); 266 | 267 | URL url = new URL("mock://localhost"); 268 | int connectTimeout = 10000; 269 | int readTimeout = 15000; 270 | 271 | //Test creation: without Proxy 272 | UrlJwkProvider urlJwkProvider = new UrlJwkProvider(url, connectTimeout, readTimeout); 273 | assertThat(urlJwkProvider.proxy, is(nullValue())); 274 | 275 | Jwk jwk = urlJwkProvider.get("NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg"); 276 | assertNotNull(jwk); 277 | assertThat(mockFactory.urlUsed.get(), is(url)); 278 | assertThat(mockFactory.proxyUsed.get(), is(nullValue())); 279 | 280 | // Test creation: custom headers 281 | UrlJwkProvider urlJwkProviderWithHeaders = new UrlJwkProvider(url, connectTimeout, readTimeout, null, 282 | Collections.singletonMap("Accept", "application/jwks-set+json")); 283 | Jwk hJwk = urlJwkProviderWithHeaders.get("NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg"); 284 | assertNotNull(hJwk); 285 | assertThat(mockFactory.urlUsed.get(), is(url)); 286 | assertThat(mockFactory.proxyUsed.get(), is(nullValue())); 287 | 288 | //Test creation: with Proxy 289 | Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", 8080)); 290 | URL pUrl = new URL("mock://localhost"); 291 | UrlJwkProvider pUrlJwkProvider = new UrlJwkProvider(pUrl, connectTimeout, readTimeout, proxy); 292 | assertThat(pUrlJwkProvider.proxy, is(proxy)); 293 | 294 | Jwk pJwk = pUrlJwkProvider.get("NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg"); 295 | assertNotNull(pJwk); 296 | assertThat(mockFactory.urlUsed.get(), is(pUrl)); 297 | Proxy usedProxy = mockFactory.proxyUsed.get(); 298 | assertThat(usedProxy, is(notNullValue())); 299 | assertThat(usedProxy.address(), is(proxy.address())); 300 | 301 | //Test 1: Configuration 302 | //Request Timeout assertions 303 | ArgumentCaptor connectTimeoutCaptor = ArgumentCaptor.forClass(Integer.class); 304 | verify(urlConnection, times(3)).setConnectTimeout(connectTimeoutCaptor.capture()); 305 | assertThat(connectTimeoutCaptor.getValue(), is(connectTimeout)); 306 | 307 | ArgumentCaptor readTimeoutCaptor = ArgumentCaptor.forClass(Integer.class); 308 | verify(urlConnection, times(3)).setReadTimeout(readTimeoutCaptor.capture()); 309 | assertThat(readTimeoutCaptor.getValue(), is(readTimeout)); 310 | 311 | //Request Headers assertions 312 | verify(urlConnection, times(2)).setRequestProperty("Accept", "application/json"); 313 | verify(urlConnection, times(1)).setRequestProperty("Accept", "application/jwks-set+json"); 314 | 315 | //Test 2: Network errors 316 | Exception capturedException = null; 317 | try { 318 | IOException exception = mock(IOException.class); 319 | when(urlConnection.getInputStream()).thenThrow(exception); 320 | urlJwkProvider.get("NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg"); 321 | } catch (Exception e) { 322 | capturedException = e; 323 | } 324 | 325 | assertThat(capturedException, is(notNullValue())); 326 | assertThat(capturedException, is(instanceOf(NetworkException.class))); 327 | 328 | //release 329 | mockFactory.clear(); 330 | } 331 | } -------------------------------------------------------------------------------- /src/test/java/com/auth0/jwk/UtilTest.java: -------------------------------------------------------------------------------- 1 | package com.auth0.jwk; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class UtilTest { 8 | @Test 9 | public void isNullOrEmpty() { 10 | assertTrue(Util.isNullOrEmpty(null)); 11 | assertTrue(Util.isNullOrEmpty("")); 12 | assertFalse(Util.isNullOrEmpty("not empty")); 13 | } 14 | 15 | @Test 16 | public void checkArgument() { 17 | String message = "exception test"; 18 | assertThrows(message, IllegalArgumentException.class, () -> { 19 | Util.checkArgument(false, message); 20 | }); 21 | Util.checkArgument(true, message); 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/resources/empty-jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [] 3 | } -------------------------------------------------------------------------------- /src/test/resources/invalid-jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "key": "value" 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /src/test/resources/jwks-single-no-kid.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "x5c": [ 8 | "MIIDCzCCAfOgAwIBAgIJAJP6qydiMpsuMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNVBAMMEXNhbXBsZXMuYXV0aDAuY29tMB4XDTE0MDUyNjIyMDA1MFoXDTI4MDIwMjIyMDA1MFowHDEaMBgGA1UEAwwRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkH4CFGSJ4s3mwCBzaGGwxa9Jxzfb1ia4nUumxbsuaB7PClZZgrNQiOR3MXVNV9W6F1D+wjT6oFHOo7TOkVI22I/ff3XZTE0F35UUHGWRtiQ4LdZxwOPTed2Lax3F2DEyl3Y0CguUKbq2sSghvHYcggM6aj3N53VBsnBh/kdrURDLx1RYqBIL6Fvkhb/V/v/u9UKhZM0CDQRef9FZ7R8q9ie9cnbDOj1dT9d64kiJIYtTraG0gOrs4LI+4KK0EZu5R7Uo053IK7kfNasWhDkl8yxNYkDxwfcIuAcDmLgLnAI4tfW5beJuw+/w75PO/EwzwsnvppXaAz7e3Wf8g1yWFAgMBAAGjUDBOMB0GA1UdDgQWBBTsmytFLNox+NUZdTNlCUL3hHrngTAfBgNVHSMEGDAWgBTsmytFLNox+NUZdTNlCUL3hHrngTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAodbRX/34LnWB70l8dpDF1neDoG29F0XdpE9ICWHeWB1gb/FvJ5UMy9/pnL0DI3mPwkTDDob+16Zc68o6dT6sH3vEUP1iRreJlFADEmJZjrH9P4Y7ttx3G2Uw2RU5uucXIqiyMDBrQo4vx4Lnghl+b/WYbZJgzLfZLgkOEjcznS0Yi5Wdz6MvaL3FehSfweHyrjmxz0e8elHq7VY8OqRA+4PmUBce9BgDCk9fZFjgj8l0m9Vc5pPKSY9LMmTyrYkeDr/KppqdXKOCHmv7AIGb6rMCtbkIL/CM7Bh9Hx78/UKAz87Sl9A1yXVNjKbZwOEW60ORIwJmd8Tv46gJF+/rV" 9 | ], 10 | "n": "pB-AhRkieLN5sAgc2hhsMWvScc329YmuJ1LpsW7LmgezwpWWYKzUIjkdzF1TVfVuhdQ_sI0-qBRzqO0zpFSNtiP33912UxNBd-VFBxlkbYkOC3WccDj03ndi2sdxdgxMpd2NAoLlCm6trEoIbx2HIIDOmo9zed1QbJwYf5Ha1EQy8dUWKgSC-hb5IW_1f7_7vVCoWTNAg0EXn_RWe0fKvYnvXJ2wzo9XU_XeuJIiSGLU62htIDq7OCyPuCitBGbuUe1KNOdyCu5HzWrFoQ5JfMsTWJA8cH3CLgHA5i4C5wCOLX1uW3ibsPv8O-TzvxMM8LJ76aV2gM-3t1n_INclhQ", 11 | "e": "AQAB", 12 | "x5t": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/test/resources/jwks-single.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "x5c": [ 8 | "MIIDCzCCAfOgAwIBAgIJAJP6qydiMpsuMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNVBAMMEXNhbXBsZXMuYXV0aDAuY29tMB4XDTE0MDUyNjIyMDA1MFoXDTI4MDIwMjIyMDA1MFowHDEaMBgGA1UEAwwRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkH4CFGSJ4s3mwCBzaGGwxa9Jxzfb1ia4nUumxbsuaB7PClZZgrNQiOR3MXVNV9W6F1D+wjT6oFHOo7TOkVI22I/ff3XZTE0F35UUHGWRtiQ4LdZxwOPTed2Lax3F2DEyl3Y0CguUKbq2sSghvHYcggM6aj3N53VBsnBh/kdrURDLx1RYqBIL6Fvkhb/V/v/u9UKhZM0CDQRef9FZ7R8q9ie9cnbDOj1dT9d64kiJIYtTraG0gOrs4LI+4KK0EZu5R7Uo053IK7kfNasWhDkl8yxNYkDxwfcIuAcDmLgLnAI4tfW5beJuw+/w75PO/EwzwsnvppXaAz7e3Wf8g1yWFAgMBAAGjUDBOMB0GA1UdDgQWBBTsmytFLNox+NUZdTNlCUL3hHrngTAfBgNVHSMEGDAWgBTsmytFLNox+NUZdTNlCUL3hHrngTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAodbRX/34LnWB70l8dpDF1neDoG29F0XdpE9ICWHeWB1gb/FvJ5UMy9/pnL0DI3mPwkTDDob+16Zc68o6dT6sH3vEUP1iRreJlFADEmJZjrH9P4Y7ttx3G2Uw2RU5uucXIqiyMDBrQo4vx4Lnghl+b/WYbZJgzLfZLgkOEjcznS0Yi5Wdz6MvaL3FehSfweHyrjmxz0e8elHq7VY8OqRA+4PmUBce9BgDCk9fZFjgj8l0m9Vc5pPKSY9LMmTyrYkeDr/KppqdXKOCHmv7AIGb6rMCtbkIL/CM7Bh9Hx78/UKAz87Sl9A1yXVNjKbZwOEW60ORIwJmd8Tv46gJF+/rV" 9 | ], 10 | "n": "pB-AhRkieLN5sAgc2hhsMWvScc329YmuJ1LpsW7LmgezwpWWYKzUIjkdzF1TVfVuhdQ_sI0-qBRzqO0zpFSNtiP33912UxNBd-VFBxlkbYkOC3WccDj03ndi2sdxdgxMpd2NAoLlCm6trEoIbx2HIIDOmo9zed1QbJwYf5Ha1EQy8dUWKgSC-hb5IW_1f7_7vVCoWTNAg0EXn_RWe0fKvYnvXJ2wzo9XU_XeuJIiSGLU62htIDq7OCyPuCitBGbuUe1KNOdyCu5HzWrFoQ5JfMsTWJA8cH3CLgHA5i4C5wCOLX1uW3ibsPv8O-TzvxMM8LJ76aV2gM-3t1n_INclhQ", 11 | "e": "AQAB", 12 | "kid": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg", 13 | "x5t": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/test/resources/jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "alg": "RS256", 5 | "kty": "RSA", 6 | "use": "sig", 7 | "x5c": [ 8 | "MIIDCzCCAfOgAwIBAgIJAJP6qydiMpsuMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNVBAMMEXNhbXBsZXMuYXV0aDAuY29tMB4XDTE0MDUyNjIyMDA1MFoXDTI4MDIwMjIyMDA1MFowHDEaMBgGA1UEAwwRc2FtcGxlcy5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkH4CFGSJ4s3mwCBzaGGwxa9Jxzfb1ia4nUumxbsuaB7PClZZgrNQiOR3MXVNV9W6F1D+wjT6oFHOo7TOkVI22I/ff3XZTE0F35UUHGWRtiQ4LdZxwOPTed2Lax3F2DEyl3Y0CguUKbq2sSghvHYcggM6aj3N53VBsnBh/kdrURDLx1RYqBIL6Fvkhb/V/v/u9UKhZM0CDQRef9FZ7R8q9ie9cnbDOj1dT9d64kiJIYtTraG0gOrs4LI+4KK0EZu5R7Uo053IK7kfNasWhDkl8yxNYkDxwfcIuAcDmLgLnAI4tfW5beJuw+/w75PO/EwzwsnvppXaAz7e3Wf8g1yWFAgMBAAGjUDBOMB0GA1UdDgQWBBTsmytFLNox+NUZdTNlCUL3hHrngTAfBgNVHSMEGDAWgBTsmytFLNox+NUZdTNlCUL3hHrngTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAodbRX/34LnWB70l8dpDF1neDoG29F0XdpE9ICWHeWB1gb/FvJ5UMy9/pnL0DI3mPwkTDDob+16Zc68o6dT6sH3vEUP1iRreJlFADEmJZjrH9P4Y7ttx3G2Uw2RU5uucXIqiyMDBrQo4vx4Lnghl+b/WYbZJgzLfZLgkOEjcznS0Yi5Wdz6MvaL3FehSfweHyrjmxz0e8elHq7VY8OqRA+4PmUBce9BgDCk9fZFjgj8l0m9Vc5pPKSY9LMmTyrYkeDr/KppqdXKOCHmv7AIGb6rMCtbkIL/CM7Bh9Hx78/UKAz87Sl9A1yXVNjKbZwOEW60ORIwJmd8Tv46gJF+/rV" 9 | ], 10 | "n": "pB-AhRkieLN5sAgc2hhsMWvScc329YmuJ1LpsW7LmgezwpWWYKzUIjkdzF1TVfVuhdQ_sI0-qBRzqO0zpFSNtiP33912UxNBd-VFBxlkbYkOC3WccDj03ndi2sdxdgxMpd2NAoLlCm6trEoIbx2HIIDOmo9zed1QbJwYf5Ha1EQy8dUWKgSC-hb5IW_1f7_7vVCoWTNAg0EXn_RWe0fKvYnvXJ2wzo9XU_XeuJIiSGLU62htIDq7OCyPuCitBGbuUe1KNOdyCu5HzWrFoQ5JfMsTWJA8cH3CLgHA5i4C5wCOLX1uW3ibsPv8O-TzvxMM8LJ76aV2gM-3t1n_INclhQ", 11 | "e": "AQAB", 12 | "kid": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg", 13 | "x5t": "NkJCQzIyQzRBMEU4NjhGNUU4MzU4RkY0M0ZDQzkwOUQ0Q0VGNUMwQg" 14 | }, 15 | { 16 | "alg": "RS256", 17 | "kty": "RSA", 18 | "use": "sig", 19 | "x5c": [ 20 | "MIIC8DCCAdigAwIBAgIJ4pL5sRgcIYGZMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNVBAMTFGxiYWxtYWNlZGEuYXV0aDAuY29tMB4XDTE1MTIxMjE5MDczM1oXDTI5MDgyMDE5MDczM1owHzEdMBsGA1UEAxMUbGJhbG1hY2VkYS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPoo5DA/X8suAZujdmD2D88Ggtu8G/kuLUdEuj1W3+wzmFcEqQpE532rg8L0uppWKAbmLWzkuwyioNDhWwCtXnug3BFQf5Lrc6nTxjk4ZQt/HdsYWCGSSZueMUG/3I+2PSql3atD2nedjY6Z9hWU8kzOjF9wzkLMgPf/OYpuz9A+6d+/K8jApRPfsQ1LDVWDG8YRtj+IyHhSvXS+cK03iuD7yVLKkIZuoS8ymMJpnZONHGds/3P9pHY29KqliSYW0eGEX3BIarZG06gRJ+88WUbRi9+rfVAoGLq++S+bc021txK+qYS3nknhY0uv/ODBb4eeycuDjjdyLBCShVvbXFAgMBAAGjLzAtMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFG38TTjyzhRmpK7MXfvBXDcBtYJ3MA0GCSqGSIb3DQEBBQUAA4IBAQCLNW+rA25tjHs6Sa9VPgBfMMLd1PIEgMpQhET9JqpGYUgB+0q1leXw1cwh14x/6PF2oo3jPOMW+wCDA7KAVKYewYSr/Enph+zNFPaq2YQL9dCsVFcBsnEGznwZaqHrqxQDX9S2Ek6E9jNsuBCSpAPcTsfbn2TXz77V+HZ/4tbwRvYEX1S5agiZFyjZzJMiZU1KQzP5PhfzD6RPl5KTK2PYRhVdXwyuFxOdJzCzOC9E/Uw30Zd6+9oHmoNfvJr8BRy67YWjXaQAh2m8e+zv/dEzPimgvaLmI1yz4W+93dJy3NdMuCvObOqA534tviv5PkV57ewXAnWPbxyBHr57HdQ1" 21 | ], 22 | "n": "z6KOQwP1_LLgGbo3Zg9g_PBoLbvBv5Li1HRLo9Vt_sM5hXBKkKROd9q4PC9LqaVigG5i1s5LsMoqDQ4VsArV57oNwRUH-S63Op08Y5OGULfx3bGFghkkmbnjFBv9yPtj0qpd2rQ9p3nY2OmfYVlPJMzoxfcM5CzID3_zmKbs_QPunfvyvIwKUT37ENSw1VgxvGEbY_iMh4Ur10vnCtN4rg-8lSypCGbqEvMpjCaZ2TjRxnbP9z_aR2NvSqpYkmFtHhhF9wSGq2RtOoESfvPFlG0Yvfq31QKBi6vvkvm3NNtbcSvqmEt55J4WNLr_zgwW-HnsnLg443ciwQkoVb21xQ", 23 | "e": "AQAB", 24 | "kid": "RUVBOTVEMEZBMTA5NDAzNEQzNTZGNzMyMTI4MzU1RkNFQzhCQTM0Mg", 25 | "x5t": "RUVBOTVEMEZBMTA5NDAzNEQzNTZGNzMyMTI4MzU1RkNFQzhCQTM0Mg" 26 | } 27 | ] 28 | } --------------------------------------------------------------------------------