├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml ├── actions │ ├── build │ │ └── action.yml │ ├── get-prerelease │ │ └── action.yml │ ├── get-release-notes │ │ └── action.yml │ ├── get-version │ │ └── action.yml │ ├── npm-publish │ │ └── action.yml │ ├── release-create │ │ └── action.yml │ ├── rl-scanner │ │ └── action.yml │ └── tag-exists │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── npm-release.yml │ ├── publish.yml │ ├── rl-secure.yml │ ├── semgrep.yml │ ├── snyk.yml │ └── test.yml ├── .gitignore ├── .semgrepignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── search.js │ └── style.css ├── classes │ ├── InsufficientScopeError.html │ ├── InvalidRequestError.html │ ├── InvalidTokenError.html │ └── UnauthorizedError.html ├── functions │ ├── auth.html │ ├── claimCheck.html │ ├── claimEquals.html │ ├── claimIncludes.html │ ├── requiredScopes.html │ └── scopeIncludesAny.html ├── index.html ├── interfaces │ ├── AuthOptions.html │ ├── AuthResult.html │ ├── JWTHeader.html │ └── JWTPayload.html └── types │ └── JSONPrimitive.html ├── opslevel.yml ├── package-lock.json ├── package.json ├── packages ├── access-token-jwt │ ├── .eslintrc │ ├── .prettierrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── claim-check.ts │ │ ├── discovery.ts │ │ ├── fetch.ts │ │ ├── get-key-fn.ts │ │ ├── index.ts │ │ ├── jwt-verifier.ts │ │ └── validate.ts │ ├── test │ │ ├── claim-check.test.ts │ │ ├── discovery.test.ts │ │ ├── get-key-fn.test.ts │ │ ├── helpers.ts │ │ ├── index.test.ts │ │ ├── jwt-verifier.test.ts │ │ └── validate.test.ts │ └── tsconfig.json ├── examples │ ├── .prettierrc │ ├── express-api.ts │ ├── index.ts │ ├── package.json │ ├── playground.ejs │ ├── playground.ts │ ├── secret.ts │ └── tsconfig.json ├── express-oauth2-jwt-bearer │ ├── .eslintrc │ ├── .prettierrc │ ├── .shiprc │ ├── .version │ ├── CHANGELOG.md │ ├── EXAMPLES.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ └── index.ts │ ├── test │ │ └── index.test.ts │ ├── tsconfig.json │ └── typedoc.js └── oauth2-bearer │ ├── .eslintrc │ ├── .prettierrc │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── errors.ts │ ├── get-token.ts │ └── index.ts │ ├── test │ ├── errors.test.ts │ └── get-token.test.ts │ └── tsconfig.json ├── tsconfig.typedoc.json └── typedoc.js /.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/node-oauth2-jwt-bearer/tree/main/packages/express-oauth2-jwt-bearer#readme) and [Examples](https://github.com/auth0/node-oauth2-jwt-bearer/blob/main/packages/express-oauth2-jwt-bearer/EXAMPLES.md), and have not found a suitable solution or answer. 17 | required: true 18 | - label: I have searched the [issues](https://github.com/auth0/node-oauth2-jwt-bearer/issues) and have not found a suitable solution or answer. 19 | required: true 20 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 21 | required: true 22 | - 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). 23 | required: true 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description 29 | description: Provide a clear and concise description of the issue, including what you expected to happen. 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: reproduction 35 | attributes: 36 | label: Reproduction 37 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 38 | placeholder: | 39 | 1. Step 1... 40 | 2. Step 2... 41 | 3. ... 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: additional-context 47 | attributes: 48 | label: Additional context 49 | description: Other libraries that might be involved, or any other relevant information you think would be useful. 50 | validations: 51 | required: false 52 | 53 | - type: input 54 | id: environment-version 55 | attributes: 56 | label: express-oauth2-jwt-bearer version 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | id: environment-nodejs-version 62 | attributes: 63 | label: Node.js version 64 | validations: 65 | required: true 66 | -------------------------------------------------------------------------------- /.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/node-oauth2-jwt-bearer/tree/main/packages/express-oauth2-jwt-bearer#readme) and [Examples](https://github.com/auth0/node-oauth2-jwt-bearer/blob/main/packages/express-oauth2-jwt-bearer/EXAMPLES.md), and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have searched the [issues](https://github.com/auth0/node-oauth2-jwt-bearer/issues) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 16 | required: true 17 | - 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). 18 | required: true 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Describe the problem you'd like to have solved 24 | description: A clear and concise description of what the problem is. 25 | placeholder: I'm always frustrated when... 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: ideal-solution 31 | attributes: 32 | label: Describe the ideal solution 33 | description: A clear and concise description of what you want to happen. 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: alternatives-and-workarounds 39 | attributes: 40 | label: Alternatives and current workarounds 41 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 42 | validations: 43 | required: false 44 | 45 | - type: textarea 46 | id: additional-context 47 | attributes: 48 | label: Additional context 49 | description: Add any other context or screenshots about the feature request here. 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.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 express-oauth2-jwt-bearer in the Auth0 Community forums 6 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | description: Build the SDK package 3 | 4 | inputs: 5 | node: 6 | description: The Node version to use 7 | required: false 8 | default: 18 9 | 10 | runs: 11 | using: composite 12 | 13 | steps: 14 | - name: Setup Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ inputs.node }} 18 | cache: npm 19 | 20 | - name: Install dependencies 21 | shell: bash 22 | run: npm ci 23 | env: 24 | NODE_ENV: development 25 | 26 | - name: Build package 27 | shell: bash 28 | run: npm run build 29 | -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /.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 | inputs: 10 | working-directory: 11 | default: './' 12 | 13 | outputs: 14 | version: 15 | value: ${{ steps.get_version.outputs.VERSION }} 16 | 17 | runs: 18 | using: composite 19 | 20 | steps: 21 | - id: get_version 22 | shell: bash 23 | working-directory: ${{ inputs.working-directory }} 24 | run: | 25 | VERSION=$(head -1 .version) 26 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT -------------------------------------------------------------------------------- /.github/actions/npm-publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish release to npm 2 | 3 | inputs: 4 | node-version: 5 | required: true 6 | npm-token: 7 | required: true 8 | version: 9 | required: true 10 | require-build: 11 | default: true 12 | release-directory: 13 | default: './' 14 | 15 | runs: 16 | using: composite 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ inputs.node-version }} 26 | cache: 'npm' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: Install dependencies 30 | shell: bash 31 | run: npm ci --include=dev 32 | 33 | - name: Build package 34 | if: inputs.require-build == 'true' 35 | shell: bash 36 | run: npm run build 37 | 38 | - name: Publish release to NPM 39 | shell: bash 40 | working-directory: ${{ inputs.release-directory }} 41 | run: | 42 | if [[ "${VERSION}" == *"beta"* ]]; then 43 | TAG="beta" 44 | elif [[ "${VERSION}" == *"alpha"* ]]; then 45 | TAG="alpha" 46 | else 47 | TAG="latest" 48 | fi 49 | npm publish --provenance --tag $TAG 50 | env: 51 | NODE_AUTH_TOKEN: ${{ inputs.npm-token }} 52 | VERSION: ${{ inputs.version }} -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /.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/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: "37 10 * * 2" 14 | 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 22 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 23 | 24 | jobs: 25 | analyze: 26 | name: Check for Vulnerabilities 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [javascript] 33 | 34 | steps: 35 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 36 | 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. 37 | 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | queries: +security-and-quality 46 | 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v3 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v3 52 | with: 53 | category: "/language:${{ matrix.language }}" 54 | -------------------------------------------------------------------------------- /.github/workflows/npm-release.yml: -------------------------------------------------------------------------------- 1 | name: Create npm and GitHub Release 2 | 3 | on: 4 | 5 | workflow_call: 6 | inputs: 7 | node-version: 8 | required: true 9 | type: string 10 | require-build: 11 | default: true 12 | type: string 13 | release-directory: 14 | default: './' 15 | type: string 16 | secrets: 17 | github-token: 18 | required: true 19 | npm-token: 20 | required: true 21 | 22 | jobs: 23 | release: 24 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 25 | runs-on: ubuntu-latest 26 | environment: release 27 | 28 | steps: 29 | # Checkout the code 30 | - uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | # Get the version from the branch name 35 | - id: get_version 36 | uses: ./.github/actions/get-version 37 | with: 38 | working-directory: ${{ inputs.release-directory }} 39 | 40 | 41 | # Get the prerelease flag from the branch name 42 | - id: get_prerelease 43 | uses: ./.github/actions/get-prerelease 44 | with: 45 | version: ${{ steps.get_version.outputs.version }} 46 | 47 | # Get the release notes 48 | - id: get_release_notes 49 | uses: ./.github/actions/get-release-notes 50 | with: 51 | token: ${{ secrets.github-token }} 52 | version: ${{ steps.get_version.outputs.version }} 53 | repo_owner: ${{ github.repository_owner }} 54 | repo_name: ${{ github.event.repository.name }} 55 | 56 | # Check if the tag already exists 57 | - id: tag_exists 58 | uses: ./.github/actions/tag-exists 59 | with: 60 | tag: ${{ steps.get_version.outputs.version }} 61 | token: ${{ secrets.github-token }} 62 | 63 | # If the tag already exists, exit with an error 64 | - if: steps.tag_exists.outputs.exists == 'true' 65 | run: exit 1 66 | 67 | # Publish the release to our package manager 68 | - uses: ./.github/actions/npm-publish 69 | with: 70 | node-version: ${{ inputs.node-version }} 71 | require-build: ${{ inputs.require-build }} 72 | version: ${{ steps.get_version.outputs.version }} 73 | npm-token: ${{ secrets.npm-token }} 74 | release-directory: ${{ inputs.release-directory }} 75 | 76 | # Create a release for the tag 77 | - uses: ./.github/actions/release-create 78 | with: 79 | token: ${{ secrets.github-token }} 80 | name: ${{ steps.get_version.outputs.version }} 81 | body: ${{ steps.get_release_notes.outputs.release-notes }} 82 | tag: ${{ steps.get_version.outputs.version }} 83 | commit: ${{ github.sha }} 84 | prerelease: ${{ steps.get_prerelease.outputs.prerelease }} -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | id-token: write # For publishing to npm using --provenance 12 | 13 | jobs: 14 | rl-scanner: 15 | uses: ./.github/workflows/rl-secure.yml 16 | with: 17 | node-version: 18 18 | artifact-name: "node-oauth2-jwt-bearer.tgz" 19 | secrets: 20 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 21 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 22 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 23 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 24 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 25 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 26 | 27 | release: 28 | uses: ./.github/workflows/npm-release.yml 29 | needs: rl-scanner 30 | with: 31 | node-version: 18 32 | require-build: true 33 | release-directory: "./packages/express-oauth2-jwt-bearer" 34 | secrets: 35 | npm-token: ${{ secrets.NPM_TOKEN }} 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/rl-secure.yml: -------------------------------------------------------------------------------- 1 | name: RL-Secure Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node-version: ## depends if build requires node else we can remove this. 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 | rl-scanner: 28 | name: Run Reversing Labs Scanner 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 | outputs: 32 | scan-status: ${{ steps.rl-scan-conclusion.outcome }} 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Build package 41 | uses: ./.github/actions/build 42 | with: 43 | node: ${{ inputs.node-version }} 44 | 45 | - name: Create tgz build artifact 46 | run: | 47 | tar -czvf ${{ inputs.artifact-name }} * 48 | 49 | - id: get_version 50 | uses: ./.github/actions/get-version 51 | with: 52 | working-directory: "./packages/express-oauth2-jwt-bearer" 53 | 54 | - name: Run RL Scanner 55 | id: rl-scan-conclusion 56 | uses: ./.github/actions/rl-scanner 57 | with: 58 | artifact-path: "$(pwd)/${{ inputs.artifact-name }}" 59 | version: "${{ steps.get_version.outputs.version }}" 60 | env: 61 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 62 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 63 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 64 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 65 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 66 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 67 | 68 | - name: Output scan result 69 | run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV 70 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: "30 0 1,15 * *" 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 20 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 21 | 22 | jobs: 23 | 24 | run: 25 | 26 | name: Check for Vulnerabilities 27 | runs-on: ubuntu-latest 28 | 29 | container: 30 | image: returntocorp/semgrep 31 | 32 | steps: 33 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 34 | 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. 35 | 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 39 | 40 | - run: semgrep ci 41 | env: 42 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 43 | -------------------------------------------------------------------------------- /.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 | - main 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/main' }} 22 | 23 | jobs: 24 | 25 | check: 26 | 27 | name: Check for Vulnerabilities 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 32 | 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. 33 | 34 | - uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 37 | 38 | - uses: snyk/actions/node@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0 39 | env: 40 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 41 | with: 42 | args: --all-projects 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | merge_group: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 19 | 20 | env: 21 | NODE_VERSION: 18 22 | CACHE_KEY: "${{ github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}" 23 | 24 | jobs: 25 | build: 26 | name: Build Package 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - uses: ./.github/actions/build 34 | with: 35 | node: ${{ env.NODE_VERSION }} 36 | 37 | - name: Save build artifacts 38 | uses: actions/cache/save@v4 39 | with: 40 | path: . 41 | key: ${{ env.CACHE_KEY }} 42 | 43 | unit: 44 | needs: build # Require build to complete before running tests 45 | 46 | name: Run Unit Tests 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - uses: actions/setup-node@v4 53 | with: 54 | node-version: ${{ env.NODE_VERSION }} 55 | cache: npm 56 | 57 | - uses: actions/cache/restore@v4 58 | with: 59 | path: . 60 | key: ${{ env.CACHE_KEY }} 61 | 62 | - run: npm run test 63 | 64 | - uses: codecov/codecov-action@4fe8c5f003fae66aa5ebb77cfd3e7bfbbda0b6b0 # pin@3.1.5 65 | 66 | lint: 67 | needs: build # Require build to complete before running tests 68 | 69 | name: Lint Code 70 | runs-on: ubuntu-latest 71 | 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - uses: actions/setup-node@v4 76 | with: 77 | node-version: ${{ env.NODE_VERSION }} 78 | cache: npm 79 | 80 | - uses: actions/cache/restore@v4 81 | with: 82 | path: . 83 | key: ${{ env.CACHE_KEY }} 84 | 85 | - run: npm run lint 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | test-results 5 | *.code-workspace -------------------------------------------------------------------------------- /.semgrepignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions can be made to this library through PRs to fix issues, improve documentation or add features. Please fork this repo, create a well-named branch, and submit a PR with a complete template filled out. 4 | 5 | Code changes in PRs should be accompanied by tests covering the changed or added functionality. Tests can be run for this library with: 6 | 7 | ```bash 8 | npm install 9 | npm test 10 | ``` 11 | 12 | When you're ready to push your changes, please run the lint command first: 13 | 14 | ```bash 15 | npm run lint 16 | ``` 17 | 18 | ## Developing 19 | 20 | This monorepo uses npm workspaces. You must have `npm >= @7.14` to develop this package 21 | 22 | To run a command in the context of a workspace from the root use the `--workspace` or `--workspaces` arguments. 23 | 24 | ```shell 25 | # install jose on access-token-jwt 26 | npm run install jose --workspace=access-token-jwt 27 | 28 | # build oauth2-bearer 29 | npm run build --workspace=oauth2-bearer 30 | 31 | # run all tests 32 | npm test --workspaces # you can also use the `npm test` script 33 | ``` 34 | 35 | ### Playground app 36 | 37 | ```shell 38 | npm run dev --workspace=packages/examples 39 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Auth0 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 | # oauth2-jwt-bearer 2 | 3 | Monorepo for `oauth2-jwt-bearer`. Contains the following packages: 4 | 5 | | package | published | description | 6 | |-------------------------------------------------------------------|:---------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------| 7 | | [oauth2-bearer](./packages/oauth2-bearer) | ✘ | Gets Bearer tokens from a request and issues errors per [rfc6750](https://tools.ietf.org/html/rfc6750) | 8 | | [access-token-jwt](./packages/access-token-jwt) | ✘ | Verfies and decodes Access Token JWTs loosley following [draft-ietf-oauth-access-token-jwt-12](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-12) | 9 | | [express-oauth2-jwt-bearer](./packages/express-oauth2-jwt-bearer) | ✔ | Authentication middleware for Express.js that validates JWT Bearer Access Tokens | 10 | 11 | ## Feedback 12 | 13 | ### Contributing 14 | 15 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: 16 | 17 | - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 18 | - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) 19 | - [This repo's contribution guide](https://github.com/auth0/node-oauth2-jwt-bearer/blob/main/CONTRIBUTING.md) 20 | 21 | ### Raise an issue 22 | 23 | To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/node-oauth2-jwt-bearer/issues). 24 | 25 | ### Vulnerability Reporting 26 | 27 | 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. 28 | 29 | ## What is Auth0? 30 | 31 |

32 | 33 | 34 | 35 | Auth0 Logo 36 | 37 |

38 |

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

41 |

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

-------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #000000; 3 | --dark-hl-0: #D4D4D4; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #C8C8C8; 6 | --light-hl-2: #008000; 7 | --dark-hl-2: #6A9955; 8 | --light-hl-3: #0000FF; 9 | --dark-hl-3: #569CD6; 10 | --light-hl-4: #A31515; 11 | --dark-hl-4: #CE9178; 12 | --light-hl-5: #001080; 13 | --dark-hl-5: #9CDCFE; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #0070C1; 17 | --dark-hl-7: #4FC1FF; 18 | --light-hl-8: #AF00DB; 19 | --dark-hl-8: #C586C0; 20 | --light-code-background: #FFFFFF; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | :root[data-theme='light'] { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | :root[data-theme='dark'] { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"rows\":[{\"kind\":256,\"name\":\"AuthOptions\",\"url\":\"interfaces/AuthOptions.html\",\"classes\":\"\"},{\"kind\":1024,\"name\":\"authRequired\",\"url\":\"interfaces/AuthOptions.html#authRequired\",\"classes\":\"\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"issuerBaseURL\",\"url\":\"interfaces/AuthOptions.html#issuerBaseURL\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"audience\",\"url\":\"interfaces/AuthOptions.html#audience\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"issuer\",\"url\":\"interfaces/AuthOptions.html#issuer\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"jwksUri\",\"url\":\"interfaces/AuthOptions.html#jwksUri\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"agent\",\"url\":\"interfaces/AuthOptions.html#agent\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"cooldownDuration\",\"url\":\"interfaces/AuthOptions.html#cooldownDuration\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"timeoutDuration\",\"url\":\"interfaces/AuthOptions.html#timeoutDuration\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"cacheMaxAge\",\"url\":\"interfaces/AuthOptions.html#cacheMaxAge\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"validators\",\"url\":\"interfaces/AuthOptions.html#validators\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"clockTolerance\",\"url\":\"interfaces/AuthOptions.html#clockTolerance\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"maxTokenAge\",\"url\":\"interfaces/AuthOptions.html#maxTokenAge\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"strict\",\"url\":\"interfaces/AuthOptions.html#strict\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"secret\",\"url\":\"interfaces/AuthOptions.html#secret\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":1024,\"name\":\"tokenSigningAlg\",\"url\":\"interfaces/AuthOptions.html#tokenSigningAlg\",\"classes\":\"tsd-is-inherited\",\"parent\":\"AuthOptions\"},{\"kind\":64,\"name\":\"auth\",\"url\":\"functions/auth.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"claimCheck\",\"url\":\"functions/claimCheck.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"claimEquals\",\"url\":\"functions/claimEquals.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"claimIncludes\",\"url\":\"functions/claimIncludes.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"requiredScopes\",\"url\":\"functions/requiredScopes.html\",\"classes\":\"\"},{\"kind\":64,\"name\":\"scopeIncludesAny\",\"url\":\"functions/scopeIncludesAny.html\",\"classes\":\"\"},{\"kind\":256,\"name\":\"AuthResult\",\"url\":\"interfaces/AuthResult.html\",\"classes\":\"\"},{\"kind\":1024,\"name\":\"header\",\"url\":\"interfaces/AuthResult.html#header\",\"classes\":\"\",\"parent\":\"AuthResult\"},{\"kind\":1024,\"name\":\"payload\",\"url\":\"interfaces/AuthResult.html#payload\",\"classes\":\"\",\"parent\":\"AuthResult\"},{\"kind\":1024,\"name\":\"token\",\"url\":\"interfaces/AuthResult.html#token\",\"classes\":\"\",\"parent\":\"AuthResult\"},{\"kind\":4194304,\"name\":\"JSONPrimitive\",\"url\":\"types/JSONPrimitive.html\",\"classes\":\"\"},{\"kind\":128,\"name\":\"UnauthorizedError\",\"url\":\"classes/UnauthorizedError.html\",\"classes\":\"\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/UnauthorizedError.html#constructor\",\"classes\":\"\",\"parent\":\"UnauthorizedError\"},{\"kind\":1024,\"name\":\"status\",\"url\":\"classes/UnauthorizedError.html#status\",\"classes\":\"\",\"parent\":\"UnauthorizedError\"},{\"kind\":1024,\"name\":\"statusCode\",\"url\":\"classes/UnauthorizedError.html#statusCode\",\"classes\":\"\",\"parent\":\"UnauthorizedError\"},{\"kind\":1024,\"name\":\"headers\",\"url\":\"classes/UnauthorizedError.html#headers\",\"classes\":\"\",\"parent\":\"UnauthorizedError\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/UnauthorizedError.html#headers.__type\",\"classes\":\"\",\"parent\":\"UnauthorizedError.headers\"},{\"kind\":1024,\"name\":\"WWW-Authenticate\",\"url\":\"classes/UnauthorizedError.html#headers.__type.WWW_Authenticate\",\"classes\":\"\",\"parent\":\"UnauthorizedError.headers.__type\"},{\"kind\":128,\"name\":\"InvalidRequestError\",\"url\":\"classes/InvalidRequestError.html\",\"classes\":\"\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/InvalidRequestError.html#constructor\",\"classes\":\"\",\"parent\":\"InvalidRequestError\"},{\"kind\":1024,\"name\":\"code\",\"url\":\"classes/InvalidRequestError.html#code\",\"classes\":\"\",\"parent\":\"InvalidRequestError\"},{\"kind\":1024,\"name\":\"status\",\"url\":\"classes/InvalidRequestError.html#status\",\"classes\":\"\",\"parent\":\"InvalidRequestError\"},{\"kind\":1024,\"name\":\"statusCode\",\"url\":\"classes/InvalidRequestError.html#statusCode\",\"classes\":\"\",\"parent\":\"InvalidRequestError\"},{\"kind\":1024,\"name\":\"headers\",\"url\":\"classes/InvalidRequestError.html#headers\",\"classes\":\"tsd-is-inherited\",\"parent\":\"InvalidRequestError\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/InvalidRequestError.html#headers.__type\",\"classes\":\"\",\"parent\":\"InvalidRequestError.headers\"},{\"kind\":1024,\"name\":\"WWW-Authenticate\",\"url\":\"classes/InvalidRequestError.html#headers.__type.WWW_Authenticate\",\"classes\":\"\",\"parent\":\"InvalidRequestError.headers.__type\"},{\"kind\":128,\"name\":\"InvalidTokenError\",\"url\":\"classes/InvalidTokenError.html\",\"classes\":\"\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/InvalidTokenError.html#constructor\",\"classes\":\"\",\"parent\":\"InvalidTokenError\"},{\"kind\":1024,\"name\":\"code\",\"url\":\"classes/InvalidTokenError.html#code\",\"classes\":\"\",\"parent\":\"InvalidTokenError\"},{\"kind\":1024,\"name\":\"status\",\"url\":\"classes/InvalidTokenError.html#status\",\"classes\":\"\",\"parent\":\"InvalidTokenError\"},{\"kind\":1024,\"name\":\"statusCode\",\"url\":\"classes/InvalidTokenError.html#statusCode\",\"classes\":\"\",\"parent\":\"InvalidTokenError\"},{\"kind\":1024,\"name\":\"headers\",\"url\":\"classes/InvalidTokenError.html#headers\",\"classes\":\"tsd-is-inherited\",\"parent\":\"InvalidTokenError\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/InvalidTokenError.html#headers.__type\",\"classes\":\"\",\"parent\":\"InvalidTokenError.headers\"},{\"kind\":1024,\"name\":\"WWW-Authenticate\",\"url\":\"classes/InvalidTokenError.html#headers.__type.WWW_Authenticate\",\"classes\":\"\",\"parent\":\"InvalidTokenError.headers.__type\"},{\"kind\":128,\"name\":\"InsufficientScopeError\",\"url\":\"classes/InsufficientScopeError.html\",\"classes\":\"\"},{\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/InsufficientScopeError.html#constructor\",\"classes\":\"\",\"parent\":\"InsufficientScopeError\"},{\"kind\":1024,\"name\":\"code\",\"url\":\"classes/InsufficientScopeError.html#code\",\"classes\":\"\",\"parent\":\"InsufficientScopeError\"},{\"kind\":1024,\"name\":\"status\",\"url\":\"classes/InsufficientScopeError.html#status\",\"classes\":\"\",\"parent\":\"InsufficientScopeError\"},{\"kind\":1024,\"name\":\"statusCode\",\"url\":\"classes/InsufficientScopeError.html#statusCode\",\"classes\":\"\",\"parent\":\"InsufficientScopeError\"},{\"kind\":1024,\"name\":\"headers\",\"url\":\"classes/InsufficientScopeError.html#headers\",\"classes\":\"tsd-is-inherited\",\"parent\":\"InsufficientScopeError\"},{\"kind\":65536,\"name\":\"__type\",\"url\":\"classes/InsufficientScopeError.html#headers.__type\",\"classes\":\"\",\"parent\":\"InsufficientScopeError.headers\"},{\"kind\":1024,\"name\":\"WWW-Authenticate\",\"url\":\"classes/InsufficientScopeError.html#headers.__type.WWW_Authenticate\",\"classes\":\"\",\"parent\":\"InsufficientScopeError.headers.__type\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"comment\"],\"fieldVectors\":[[\"name/0\",[0,37.716]],[\"comment/0\",[]],[\"name/1\",[1,37.716]],[\"comment/1\",[]],[\"name/2\",[2,37.716]],[\"comment/2\",[]],[\"name/3\",[3,37.716]],[\"comment/3\",[]],[\"name/4\",[4,37.716]],[\"comment/4\",[]],[\"name/5\",[5,37.716]],[\"comment/5\",[]],[\"name/6\",[6,37.716]],[\"comment/6\",[]],[\"name/7\",[7,37.716]],[\"comment/7\",[]],[\"name/8\",[8,37.716]],[\"comment/8\",[]],[\"name/9\",[9,37.716]],[\"comment/9\",[]],[\"name/10\",[10,37.716]],[\"comment/10\",[]],[\"name/11\",[11,37.716]],[\"comment/11\",[]],[\"name/12\",[12,37.716]],[\"comment/12\",[]],[\"name/13\",[13,37.716]],[\"comment/13\",[]],[\"name/14\",[14,37.716]],[\"comment/14\",[]],[\"name/15\",[15,37.716]],[\"comment/15\",[]],[\"name/16\",[16,37.716]],[\"comment/16\",[]],[\"name/17\",[17,37.716]],[\"comment/17\",[]],[\"name/18\",[18,37.716]],[\"comment/18\",[]],[\"name/19\",[19,37.716]],[\"comment/19\",[]],[\"name/20\",[20,37.716]],[\"comment/20\",[]],[\"name/21\",[21,37.716]],[\"comment/21\",[]],[\"name/22\",[22,37.716]],[\"comment/22\",[]],[\"name/23\",[23,37.716]],[\"comment/23\",[]],[\"name/24\",[24,37.716]],[\"comment/24\",[]],[\"name/25\",[25,37.716]],[\"comment/25\",[]],[\"name/26\",[26,37.716]],[\"comment/26\",[]],[\"name/27\",[27,37.716]],[\"comment/27\",[]],[\"name/28\",[28,26.432]],[\"comment/28\",[]],[\"name/29\",[29,26.432]],[\"comment/29\",[]],[\"name/30\",[30,26.432]],[\"comment/30\",[]],[\"name/31\",[31,26.432]],[\"comment/31\",[]],[\"name/32\",[32,26.432]],[\"comment/32\",[]],[\"name/33\",[33,18.974,34,18.974]],[\"comment/33\",[]],[\"name/34\",[35,37.716]],[\"comment/34\",[]],[\"name/35\",[28,26.432]],[\"comment/35\",[]],[\"name/36\",[36,29.013]],[\"comment/36\",[]],[\"name/37\",[29,26.432]],[\"comment/37\",[]],[\"name/38\",[30,26.432]],[\"comment/38\",[]],[\"name/39\",[31,26.432]],[\"comment/39\",[]],[\"name/40\",[32,26.432]],[\"comment/40\",[]],[\"name/41\",[33,18.974,34,18.974]],[\"comment/41\",[]],[\"name/42\",[37,37.716]],[\"comment/42\",[]],[\"name/43\",[28,26.432]],[\"comment/43\",[]],[\"name/44\",[36,29.013]],[\"comment/44\",[]],[\"name/45\",[29,26.432]],[\"comment/45\",[]],[\"name/46\",[30,26.432]],[\"comment/46\",[]],[\"name/47\",[31,26.432]],[\"comment/47\",[]],[\"name/48\",[32,26.432]],[\"comment/48\",[]],[\"name/49\",[33,18.974,34,18.974]],[\"comment/49\",[]],[\"name/50\",[38,37.716]],[\"comment/50\",[]],[\"name/51\",[28,26.432]],[\"comment/51\",[]],[\"name/52\",[36,29.013]],[\"comment/52\",[]],[\"name/53\",[29,26.432]],[\"comment/53\",[]],[\"name/54\",[30,26.432]],[\"comment/54\",[]],[\"name/55\",[31,26.432]],[\"comment/55\",[]],[\"name/56\",[32,26.432]],[\"comment/56\",[]],[\"name/57\",[33,18.974,34,18.974]],[\"comment/57\",[]]],\"invertedIndex\":[[\"__type\",{\"_index\":32,\"name\":{\"32\":{},\"40\":{},\"48\":{},\"56\":{}},\"comment\":{}}],[\"agent\",{\"_index\":6,\"name\":{\"6\":{}},\"comment\":{}}],[\"audience\",{\"_index\":3,\"name\":{\"3\":{}},\"comment\":{}}],[\"auth\",{\"_index\":16,\"name\":{\"16\":{}},\"comment\":{}}],[\"authenticate\",{\"_index\":34,\"name\":{\"33\":{},\"41\":{},\"49\":{},\"57\":{}},\"comment\":{}}],[\"authoptions\",{\"_index\":0,\"name\":{\"0\":{}},\"comment\":{}}],[\"authrequired\",{\"_index\":1,\"name\":{\"1\":{}},\"comment\":{}}],[\"authresult\",{\"_index\":22,\"name\":{\"22\":{}},\"comment\":{}}],[\"cachemaxage\",{\"_index\":9,\"name\":{\"9\":{}},\"comment\":{}}],[\"claimcheck\",{\"_index\":17,\"name\":{\"17\":{}},\"comment\":{}}],[\"claimequals\",{\"_index\":18,\"name\":{\"18\":{}},\"comment\":{}}],[\"claimincludes\",{\"_index\":19,\"name\":{\"19\":{}},\"comment\":{}}],[\"clocktolerance\",{\"_index\":11,\"name\":{\"11\":{}},\"comment\":{}}],[\"code\",{\"_index\":36,\"name\":{\"36\":{},\"44\":{},\"52\":{}},\"comment\":{}}],[\"constructor\",{\"_index\":28,\"name\":{\"28\":{},\"35\":{},\"43\":{},\"51\":{}},\"comment\":{}}],[\"cooldownduration\",{\"_index\":7,\"name\":{\"7\":{}},\"comment\":{}}],[\"header\",{\"_index\":23,\"name\":{\"23\":{}},\"comment\":{}}],[\"headers\",{\"_index\":31,\"name\":{\"31\":{},\"39\":{},\"47\":{},\"55\":{}},\"comment\":{}}],[\"insufficientscopeerror\",{\"_index\":38,\"name\":{\"50\":{}},\"comment\":{}}],[\"invalidrequesterror\",{\"_index\":35,\"name\":{\"34\":{}},\"comment\":{}}],[\"invalidtokenerror\",{\"_index\":37,\"name\":{\"42\":{}},\"comment\":{}}],[\"issuer\",{\"_index\":4,\"name\":{\"4\":{}},\"comment\":{}}],[\"issuerbaseurl\",{\"_index\":2,\"name\":{\"2\":{}},\"comment\":{}}],[\"jsonprimitive\",{\"_index\":26,\"name\":{\"26\":{}},\"comment\":{}}],[\"jwksuri\",{\"_index\":5,\"name\":{\"5\":{}},\"comment\":{}}],[\"maxtokenage\",{\"_index\":12,\"name\":{\"12\":{}},\"comment\":{}}],[\"payload\",{\"_index\":24,\"name\":{\"24\":{}},\"comment\":{}}],[\"requiredscopes\",{\"_index\":20,\"name\":{\"20\":{}},\"comment\":{}}],[\"scopeincludesany\",{\"_index\":21,\"name\":{\"21\":{}},\"comment\":{}}],[\"secret\",{\"_index\":14,\"name\":{\"14\":{}},\"comment\":{}}],[\"status\",{\"_index\":29,\"name\":{\"29\":{},\"37\":{},\"45\":{},\"53\":{}},\"comment\":{}}],[\"statuscode\",{\"_index\":30,\"name\":{\"30\":{},\"38\":{},\"46\":{},\"54\":{}},\"comment\":{}}],[\"strict\",{\"_index\":13,\"name\":{\"13\":{}},\"comment\":{}}],[\"timeoutduration\",{\"_index\":8,\"name\":{\"8\":{}},\"comment\":{}}],[\"token\",{\"_index\":25,\"name\":{\"25\":{}},\"comment\":{}}],[\"tokensigningalg\",{\"_index\":15,\"name\":{\"15\":{}},\"comment\":{}}],[\"unauthorizederror\",{\"_index\":27,\"name\":{\"27\":{}},\"comment\":{}}],[\"validators\",{\"_index\":10,\"name\":{\"10\":{}},\"comment\":{}}],[\"www\",{\"_index\":33,\"name\":{\"33\":{},\"41\":{},\"49\":{},\"57\":{}},\"comment\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/functions/requiredScopes.html: -------------------------------------------------------------------------------- 1 | requiredScopes | express-oauth2-jwt-bearer
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Function requiredScopes

18 |
19 |
    20 | 21 |
  • 22 |

    Check a token's scope claim to include a number of given scopes, raises a 23 | 403 insufficient_scope error if the value of the scope claim does not 24 | include all the given scopes.

    25 |
    app.use(auth());

    app.get('/admin/edit', requiredScopes('read:admin write:admin'),
    (req, res) => { ... }); 26 |
    27 |
    28 |
    29 |

    Parameters

    30 |
      31 |
    • 32 |
      scopes: string | string[]
    33 |

    Returns Handler

36 |
37 | 51 |
69 |
-------------------------------------------------------------------------------- /docs/functions/scopeIncludesAny.html: -------------------------------------------------------------------------------- 1 | scopeIncludesAny | express-oauth2-jwt-bearer
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Function scopeIncludesAny

18 |
19 |
    20 | 21 |
  • 22 |

    Check a token's scope claim to include any of the given scopes, raises a 23 | 403 insufficient_scope error if the value of the scope claim does not 24 | include any of the given scopes.

    25 |
    app.use(auth());

    app.get('/admin/edit', scopeIncludesAny('read:msg read:admin'),
    (req, res) => { ... }); 26 |
    27 |
    28 |
    29 |

    Parameters

    30 |
      31 |
    • 32 |
      scopes: string | string[]
    33 |

    Returns Handler

36 |
37 | 51 |
69 |
-------------------------------------------------------------------------------- /docs/types/JSONPrimitive.html: -------------------------------------------------------------------------------- 1 | JSONPrimitive | express-oauth2-jwt-bearer
2 |
3 | 10 |
11 |
12 |
13 |
14 | 17 |

Type alias JSONPrimitive

18 |
JSONPrimitive: string | number | boolean | null
21 |
22 | 36 |
54 |
-------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_sdks 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-jwt-bearer", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Auth0 ", 6 | "version": "0.0.1", 7 | "workspaces": [ 8 | "packages/oauth2-bearer", 9 | "packages/access-token-jwt", 10 | "packages/express-oauth2-jwt-bearer", 11 | "packages/examples" 12 | ], 13 | "scripts": { 14 | "lint": "npm run lint --workspaces", 15 | "test": "npm test --workspaces", 16 | "build": "npm run build --workspaces", 17 | "docs": "typedoc --options typedoc.js" 18 | }, 19 | "devDependencies": { 20 | "typedoc": "^0.24.6", 21 | "typescript": "^5.0.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/access-token-jwt/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "ignorePatterns": ["*.js"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/access-token-jwt/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /packages/access-token-jwt/README.md: -------------------------------------------------------------------------------- 1 | # access-token-jwt 2 | 3 | _This package is not published_ 4 | 5 | Verfies and decodes Access Token JWTs loosley following [draft-ietf-oauth-access-token-jwt-12](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-12) 6 | -------------------------------------------------------------------------------- /packages/access-token-jwt/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | reporters: [ 4 | 'default', 5 | [ 6 | 'jest-junit', 7 | { 8 | suiteName: 'access-token-jwt', 9 | outputDirectory: '../../test-results/access-token-jwt', 10 | }, 11 | ], 12 | ], 13 | testEnvironment: 'node', 14 | collectCoverageFrom: ['src/*'], 15 | coverageThreshold: { 16 | global: { 17 | branches: 100, 18 | functions: 100, 19 | lines: 100, 20 | statements: 100, 21 | }, 22 | }, 23 | moduleNameMapper: { 24 | '^oauth2-bearer$': '/../oauth2-bearer/src/', 25 | }, 26 | transform: { 27 | '^.+\\.tsx?$': [ 28 | 'ts-jest', 29 | { 30 | tsconfig: { 31 | baseUrl: '.', 32 | paths: { 33 | 'oauth2-bearer': ['../oauth2-bearer/src'], 34 | }, 35 | }, 36 | }, 37 | ], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/access-token-jwt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "access-token-jwt", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Auth0 ", 6 | "version": "0.0.1", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "test": "jest test --coverage", 11 | "lint": "eslint --fix --ext .ts ./src", 12 | "prebuild": "rimraf dist", 13 | "build": "tsc" 14 | }, 15 | "devDependencies": { 16 | "@tsconfig/node12": "^1.0.11", 17 | "@types/jest": "^27.5.2", 18 | "@types/node": "^14.18.42", 19 | "@types/sinon": "^10.0.13", 20 | "@typescript-eslint/eslint-plugin": "^5.57.0", 21 | "@typescript-eslint/parser": "^5.57.0", 22 | "eslint": "^8.37.0", 23 | "jest": "^29.5.0", 24 | "jest-junit": "^13.2.0", 25 | "nock": "^13.3.0", 26 | "prettier": "~2.5.1", 27 | "rimraf": "^3.0.2", 28 | "sinon": "^12.0.1", 29 | "ts-jest": "^29.0.5", 30 | "tslib": "^2.5.0", 31 | "typescript": "^5.0.2" 32 | }, 33 | "dependencies": { 34 | "jose": "^4.15.5" 35 | }, 36 | "engines": { 37 | "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/claim-check.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InvalidTokenError, 3 | InsufficientScopeError, 4 | UnauthorizedError, 5 | } from 'oauth2-bearer'; 6 | import type { JWTPayload } from 'jose'; 7 | 8 | export type JSONPrimitive = string | number | boolean | null; 9 | 10 | type ClaimChecker = (payload?: JWTPayload) => void; 11 | 12 | const checkJSONPrimitive = (value: JSONPrimitive): void => { 13 | if ( 14 | typeof value !== 'string' && 15 | typeof value !== 'number' && 16 | typeof value !== 'boolean' && 17 | value !== null 18 | ) { 19 | throw new TypeError("'expected' must be a string, number, boolean or null"); 20 | } 21 | }; 22 | 23 | const isClaimIncluded = ( 24 | claim: string, 25 | expected: JSONPrimitive[], 26 | matchAll = true 27 | ): ((payload: JWTPayload) => boolean) => (payload) => { 28 | if (!(claim in payload)) { 29 | throw new InvalidTokenError(`Missing '${claim}' claim`); 30 | } 31 | 32 | let actual = payload[claim]; 33 | if (typeof actual === 'string') { 34 | actual = actual.split(' '); 35 | } else if (!Array.isArray(actual)) { 36 | return false; 37 | } 38 | 39 | actual = new Set(actual as JSONPrimitive[]); 40 | 41 | return matchAll 42 | ? expected.every(Set.prototype.has.bind(actual)) 43 | : expected.some(Set.prototype.has.bind(actual)); 44 | }; 45 | 46 | export type RequiredScopes = (scopes: string | string[]) => R; 47 | 48 | export const requiredScopes: RequiredScopes = (scopes) => { 49 | if (typeof scopes === 'string') { 50 | scopes = scopes.split(' '); 51 | } else if (!Array.isArray(scopes)) { 52 | throw new TypeError("'scopes' must be a string or array of strings"); 53 | } 54 | const fn = isClaimIncluded('scope', scopes); 55 | return claimCheck((payload) => { 56 | if (!('scope' in payload)) { 57 | throw new InsufficientScopeError( 58 | scopes as string[], 59 | "Missing 'scope' claim" 60 | ); 61 | } 62 | if (!fn(payload)) { 63 | throw new InsufficientScopeError(scopes as string[]); 64 | } 65 | return true; 66 | }); 67 | }; 68 | 69 | export const scopeIncludesAny: RequiredScopes = (scopes) => { 70 | if (typeof scopes === 'string') { 71 | scopes = scopes.split(' '); 72 | } else if (!Array.isArray(scopes)) { 73 | throw new TypeError("'scopes' must be a string or array of strings"); 74 | } 75 | const fn = isClaimIncluded('scope', scopes, false); 76 | return claimCheck((payload) => { 77 | if (!('scope' in payload)) { 78 | throw new InsufficientScopeError( 79 | scopes as string[], 80 | "Missing 'scope' claim" 81 | ); 82 | } 83 | if (!fn(payload)) { 84 | throw new InsufficientScopeError(scopes as string[]); 85 | } 86 | return true; 87 | }); 88 | }; 89 | 90 | export type ClaimIncludes = ( 91 | claim: string, 92 | ...expected: JSONPrimitive[] 93 | ) => R; 94 | 95 | export const claimIncludes: ClaimIncludes = (claim, ...expected) => { 96 | if (typeof claim !== 'string') { 97 | throw new TypeError("'claim' must be a string"); 98 | } 99 | expected.forEach(checkJSONPrimitive); 100 | 101 | return claimCheck( 102 | isClaimIncluded(claim, expected), 103 | `Unexpected '${claim}' value` 104 | ); 105 | }; 106 | 107 | export type ClaimEquals = ( 108 | claim: string, 109 | expected: JSONPrimitive 110 | ) => R; 111 | 112 | export const claimEquals: ClaimEquals = (claim, expected) => { 113 | if (typeof claim !== 'string') { 114 | throw new TypeError("'claim' must be a string"); 115 | } 116 | checkJSONPrimitive(expected); 117 | 118 | return claimCheck((payload) => { 119 | if (!(claim in payload)) { 120 | throw new InvalidTokenError(`Missing '${claim}' claim`); 121 | } 122 | return payload[claim] === expected; 123 | }, `Unexpected '${claim}' value`); 124 | }; 125 | 126 | export type ClaimCheck = ( 127 | fn: (payload: JWTPayload) => boolean, 128 | errMsg?: string 129 | ) => R; 130 | 131 | export const claimCheck: ClaimCheck = (fn, errMsg) => { 132 | if (typeof fn !== 'function') { 133 | throw new TypeError("'claimCheck' expects a function"); 134 | } 135 | 136 | return (payload?: JWTPayload) => { 137 | if (!payload) { 138 | throw new UnauthorizedError(); 139 | } 140 | if (!fn(payload)) { 141 | throw new InvalidTokenError(errMsg); 142 | } 143 | }; 144 | }; 145 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/discovery.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | import fetch from './fetch'; 3 | import { strict as assert } from 'assert'; 4 | import { JwtVerifierOptions } from './jwt-verifier'; 5 | 6 | const OIDC_DISCOVERY = '/.well-known/openid-configuration'; 7 | const OAUTH2_DISCOVERY = '/.well-known/oauth-authorization-server'; 8 | 9 | export interface IssuerMetadata { 10 | issuer: string; 11 | jwks_uri: string; 12 | id_token_signing_alg_values_supported?: string[]; 13 | [key: string]: unknown; 14 | } 15 | 16 | const assertIssuer = (data: IssuerMetadata) => 17 | assert(data.issuer, `'issuer' not found in authorization server metadata`); 18 | 19 | export type DiscoverOptions = Required< 20 | Pick 21 | > & 22 | Pick; 23 | 24 | const discover = async ({ 25 | issuerBaseURL: uri, 26 | agent, 27 | timeoutDuration, 28 | }: DiscoverOptions): Promise => { 29 | const url = new URL(uri); 30 | 31 | if (url.pathname.includes('/.well-known/')) { 32 | const data = await fetch(url, { agent, timeoutDuration }); 33 | assertIssuer(data); 34 | return data; 35 | } 36 | 37 | const pathnames = []; 38 | if (url.pathname.endsWith('/')) { 39 | pathnames.push(`${url.pathname}${OIDC_DISCOVERY.substring(1)}`); 40 | } else { 41 | pathnames.push(`${url.pathname}${OIDC_DISCOVERY}`); 42 | } 43 | if (url.pathname === '/') { 44 | pathnames.push(`${OAUTH2_DISCOVERY}`); 45 | } else { 46 | pathnames.push(`${OAUTH2_DISCOVERY}${url.pathname}`); 47 | } 48 | 49 | for (const pathname of pathnames) { 50 | try { 51 | const wellKnownUri = new URL(pathname, url); 52 | const data = await fetch(wellKnownUri, { 53 | agent, 54 | timeoutDuration, 55 | }); 56 | assertIssuer(data); 57 | return data; 58 | } catch (err) { 59 | // noop 60 | } 61 | } 62 | 63 | throw new Error('Failed to fetch authorization server metadata'); 64 | }; 65 | 66 | export default (opts: DiscoverOptions) => { 67 | let discovery: Promise | undefined; 68 | let timestamp = 0; 69 | 70 | return () => { 71 | const now = Date.now(); 72 | 73 | if (!discovery || now > timestamp + opts.cacheMaxAge) { 74 | timestamp = now; 75 | discovery = discover(opts).catch((e) => { 76 | discovery = undefined; 77 | throw e; 78 | }); 79 | } 80 | return discovery; 81 | }; 82 | }; 83 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | import { Agent as HttpAgent, get as getHttp } from 'http'; 3 | import { Agent as HttpsAgent, get as getHttps } from 'https'; 4 | import { once } from 'events'; 5 | import type { ClientRequest, IncomingMessage } from 'http'; 6 | import { TextDecoder } from 'util'; 7 | 8 | const decoder = new TextDecoder(); 9 | 10 | const concat = (...buffers: Uint8Array[]): Uint8Array => { 11 | const size = buffers.reduce((acc, { length }) => acc + length, 0); 12 | const buf = new Uint8Array(size); 13 | let i = 0; 14 | buffers.forEach((buffer) => { 15 | buf.set(buffer, i); 16 | i += buffer.length; 17 | }); 18 | return buf; 19 | }; 20 | 21 | const protocols: { 22 | [protocol: string]: (...args: Parameters) => ClientRequest; 23 | } = { 24 | 'https:': getHttps, 25 | 'http:': getHttp, 26 | }; 27 | 28 | export interface FetchOptions { 29 | agent?: HttpAgent | HttpsAgent; 30 | timeoutDuration?: number; 31 | } 32 | 33 | const fetch = async ( 34 | url: URL, 35 | { agent, timeoutDuration: timeout }: FetchOptions 36 | ): Promise => { 37 | const req = protocols[url.protocol](url.href, { 38 | agent, 39 | timeout, 40 | }); 41 | 42 | const [response] = <[IncomingMessage]>await once(req, 'response'); 43 | 44 | if (response.statusCode !== 200) { 45 | throw new Error( 46 | `Failed to fetch ${url.href}, responded with ${response.statusCode}` 47 | ); 48 | } 49 | 50 | const parts = []; 51 | for await (const part of response) { 52 | parts.push(part); 53 | } 54 | 55 | try { 56 | return JSON.parse(decoder.decode(concat(...parts))); 57 | } catch (err) { 58 | throw new Error(`Failed to parse the response from ${url.href}`); 59 | } 60 | }; 61 | 62 | export default fetch; 63 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/get-key-fn.ts: -------------------------------------------------------------------------------- 1 | import { createSecretKey } from 'crypto'; 2 | import { createRemoteJWKSet } from 'jose'; 3 | import { JwtVerifierOptions } from './jwt-verifier'; 4 | 5 | type GetKeyFn = ReturnType; 6 | 7 | export type JWKSOptions = Required< 8 | Pick< 9 | JwtVerifierOptions, 10 | 'cooldownDuration' | 'timeoutDuration' | 'cacheMaxAge' 11 | > 12 | > & 13 | Pick; 14 | 15 | export default ({ 16 | agent, 17 | cooldownDuration, 18 | timeoutDuration, 19 | cacheMaxAge, 20 | secret 21 | }: JWKSOptions) => { 22 | let getKeyFn: GetKeyFn; 23 | let prevjwksUri: string; 24 | 25 | const secretKey = secret && createSecretKey(Buffer.from(secret)); 26 | 27 | return (jwksUri: string) => { 28 | if (secretKey) return () => secretKey; 29 | if (!getKeyFn || prevjwksUri !== jwksUri) { 30 | prevjwksUri = jwksUri; 31 | getKeyFn = createRemoteJWKSet(new URL(jwksUri), { 32 | agent, 33 | cooldownDuration, 34 | timeoutDuration, 35 | cacheMaxAge, 36 | }); 37 | } 38 | return getKeyFn; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as jwtVerifier, 3 | JwtVerifierOptions, 4 | VerifyJwt, 5 | VerifyJwtResult, 6 | JWTPayload, 7 | JWSHeaderParameters as JWTHeader, 8 | } from './jwt-verifier'; 9 | export { 10 | InvalidTokenError, 11 | UnauthorizedError, 12 | InsufficientScopeError, 13 | } from 'oauth2-bearer'; 14 | export { default as discover, IssuerMetadata } from './discovery'; 15 | export { 16 | claimCheck, 17 | ClaimCheck, 18 | claimEquals, 19 | ClaimEquals, 20 | claimIncludes, 21 | ClaimIncludes, 22 | requiredScopes, 23 | RequiredScopes, 24 | scopeIncludesAny, 25 | JSONPrimitive, 26 | } from './claim-check'; 27 | export { FunctionValidator, Validator, Validators } from './validate'; 28 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/jwt-verifier.ts: -------------------------------------------------------------------------------- 1 | import { strict as assert } from 'assert'; 2 | import { Agent as HttpAgent } from 'http'; 3 | import { Agent as HttpsAgent } from 'https'; 4 | import { jwtVerify } from 'jose'; 5 | import type { JWTPayload, JWSHeaderParameters } from 'jose'; 6 | import { InvalidTokenError } from 'oauth2-bearer'; 7 | import discovery from './discovery'; 8 | import getKeyFn from './get-key-fn'; 9 | import validate, { defaultValidators, Validators } from './validate'; 10 | 11 | export interface JwtVerifierOptions { 12 | /** 13 | * Base url, used to find the authorization server's app metadata per 14 | * https://datatracker.ietf.org/doc/html/rfc8414 15 | * You can pass a full url including `.well-known` if your discovery lives at 16 | * a non standard path. 17 | * REQUIRED (if you don't include {@Link AuthOptions.jwksUri} and 18 | * {@Link AuthOptions.issuer}) 19 | * You can also provide the `ISSUER_BASE_URL` environment variable. 20 | */ 21 | issuerBaseURL?: string; 22 | 23 | /** 24 | * Expected JWT "aud" (Audience) Claim value(s). 25 | * REQUIRED: You can also provide the `AUDIENCE` environment variable. 26 | */ 27 | audience?: string | string[]; 28 | 29 | /** 30 | * Expected JWT "iss" (Issuer) Claim value. 31 | * REQUIRED (if you don't include {@Link AuthOptions.issuerBaseURL}) 32 | * You can also provide the `ISSUER` environment variable. 33 | */ 34 | issuer?: string; 35 | 36 | /** 37 | * Url for the authorization server's JWKS to find the public key to verify 38 | * an Access Token JWT signed with an asymmetric algorithm. 39 | * REQUIRED (if you don't include {@Link AuthOptions.issuerBaseURL}) 40 | * You can also provide the `JWKS_URI` environment variable. 41 | */ 42 | jwksUri?: string; 43 | 44 | /** 45 | * An instance of http.Agent or https.Agent to pass to the http.get or 46 | * https.get method options. Use when behind an http(s) proxy. 47 | */ 48 | agent?: HttpAgent | HttpsAgent; 49 | 50 | /** 51 | * Duration in ms for which no more HTTP requests to the JWKS Uri endpoint 52 | * will be triggered after a previous successful fetch. 53 | * Default is 30000. 54 | */ 55 | cooldownDuration?: number; 56 | 57 | /** 58 | * Timeout in ms for HTTP requests to the JWKS and Discovery endpoint. When 59 | * reached the request will be aborted. 60 | * Default is 5000. 61 | */ 62 | timeoutDuration?: number; 63 | 64 | /** 65 | * Maximum time (in milliseconds) between successful HTTP requests to the 66 | * JWKS and Discovery endpoint. 67 | * Default is 600000 (10 minutes). 68 | */ 69 | cacheMaxAge?: number; 70 | 71 | /** 72 | * Pass in custom validators to override the existing validation behavior on 73 | * standard claims or add new validation behavior on custom claims. 74 | * 75 | * ```js 76 | * { 77 | * validators: { 78 | * // Disable issuer validation by passing `false` 79 | * iss: false, 80 | * // Add validation for a custom claim to equal a passed in string 81 | * org_id: 'my_org_123' 82 | * // Add validation for a custom claim, by passing in a function that 83 | * // accepts: 84 | * // roles: the value of the claim 85 | * // claims: an object containing the JWTPayload 86 | * // header: an object representing the JWTHeader 87 | * roles: (roles, claims, header) => roles.includes('editor') && claims.isAdmin 88 | * } 89 | * } 90 | * ``` 91 | */ 92 | validators?: Partial; 93 | 94 | /** 95 | * Clock tolerance (in secs) used when validating the `exp` and `iat` claim. 96 | * Defaults to 5 secs. 97 | */ 98 | clockTolerance?: number; 99 | 100 | /** 101 | * Maximum age (in secs) from when a token was issued to when it can no longer 102 | * be accepted. 103 | */ 104 | maxTokenAge?: number; 105 | 106 | /** 107 | * If set to `true` the token validation will strictly follow 108 | * 'JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens' 109 | * https://datatracker.ietf.org/doc/html/draft-ietf-oauth-access-token-jwt-12 110 | * Defaults to false. 111 | */ 112 | strict?: boolean; 113 | 114 | /** 115 | * Secret to verify an Access Token JWT signed with a symmetric algorithm. 116 | * By default this SDK validates tokens signed with asymmetric algorithms. 117 | */ 118 | secret?: string; 119 | 120 | /** 121 | * You must provide this if your tokens are signed with symmetric algorithms 122 | * and it must be one of HS256, HS384 or HS512. 123 | * You may provide this if your tokens are signed with asymmetric algorithms 124 | * and, if provided, it must be one of RS256, RS384, RS512, PS256, PS384, 125 | * PS512, ES256, ES256K, ES384, ES512 or EdDSA (case-sensitive). 126 | */ 127 | tokenSigningAlg?: string; 128 | } 129 | 130 | export interface VerifyJwtResult { 131 | /** 132 | * The Access Token JWT header. 133 | */ 134 | header: JWSHeaderParameters; 135 | /** 136 | * The Access Token JWT payload. 137 | */ 138 | payload: JWTPayload; 139 | /** 140 | * The raw Access Token JWT 141 | */ 142 | token: string; 143 | } 144 | 145 | export type VerifyJwt = (jwt: string) => Promise; 146 | 147 | const ASYMMETRIC_ALGS = [ 148 | 'RS256', 149 | 'RS384', 150 | 'RS512', 151 | 'PS256', 152 | 'PS384', 153 | 'PS512', 154 | 'ES256', 155 | 'ES256K', 156 | 'ES384', 157 | 'ES512', 158 | 'EdDSA', 159 | ]; 160 | const SYMMETRIC_ALGS = ['HS256', 'HS384', 'HS512']; 161 | 162 | const jwtVerifier = ({ 163 | issuerBaseURL = process.env.ISSUER_BASE_URL as string, 164 | jwksUri = process.env.JWKS_URI as string, 165 | issuer = process.env.ISSUER as string, 166 | audience = process.env.AUDIENCE as string, 167 | secret = process.env.SECRET as string, 168 | tokenSigningAlg = process.env.TOKEN_SIGNING_ALG as string, 169 | agent, 170 | cooldownDuration = 30000, 171 | timeoutDuration = 5000, 172 | cacheMaxAge = 600000, 173 | clockTolerance = 5, 174 | maxTokenAge, 175 | strict = false, 176 | validators: customValidators, 177 | }: JwtVerifierOptions): VerifyJwt => { 178 | let validators: Validators; 179 | let allowedSigningAlgs: string[] | undefined; 180 | 181 | assert( 182 | issuerBaseURL || (issuer && jwksUri) || (issuer && secret), 183 | "You must provide an 'issuerBaseURL', an 'issuer' and 'jwksUri' or an 'issuer' and 'secret'" 184 | ); 185 | assert( 186 | !(secret && jwksUri), 187 | "You must not provide both a 'secret' and 'jwksUri'" 188 | ); 189 | assert(audience, "An 'audience' is required to validate the 'aud' claim"); 190 | assert( 191 | !secret || (secret && tokenSigningAlg), 192 | "You must provide a 'tokenSigningAlg' for validating symmetric algorithms" 193 | ); 194 | assert( 195 | secret || !tokenSigningAlg || ASYMMETRIC_ALGS.includes(tokenSigningAlg), 196 | `You must supply one of ${ASYMMETRIC_ALGS.join( 197 | ', ' 198 | )} for 'tokenSigningAlg' to validate asymmetrically signed tokens` 199 | ); 200 | assert( 201 | !secret || (tokenSigningAlg && SYMMETRIC_ALGS.includes(tokenSigningAlg)), 202 | `You must supply one of ${SYMMETRIC_ALGS.join( 203 | ', ' 204 | )} for 'tokenSigningAlg' to validate symmetrically signed tokens` 205 | ); 206 | 207 | const getDiscovery = discovery({ 208 | issuerBaseURL, 209 | agent, 210 | timeoutDuration, 211 | cacheMaxAge, 212 | }); 213 | 214 | const getKeyFnGetter = getKeyFn({ 215 | agent, 216 | cooldownDuration, 217 | timeoutDuration, 218 | cacheMaxAge, 219 | secret, 220 | }); 221 | 222 | return async (jwt: string) => { 223 | try { 224 | if (issuerBaseURL) { 225 | const { 226 | jwks_uri: discoveredJwksUri, 227 | issuer: discoveredIssuer, 228 | id_token_signing_alg_values_supported: 229 | idTokenSigningAlgValuesSupported, 230 | } = await getDiscovery(); 231 | jwksUri = jwksUri || discoveredJwksUri; 232 | issuer = issuer || discoveredIssuer; 233 | allowedSigningAlgs = idTokenSigningAlgValuesSupported; 234 | } 235 | validators ||= { 236 | ...defaultValidators( 237 | issuer, 238 | audience, 239 | clockTolerance, 240 | maxTokenAge, 241 | strict, 242 | allowedSigningAlgs, 243 | tokenSigningAlg 244 | ), 245 | ...customValidators, 246 | }; 247 | const { payload, protectedHeader: header } = await jwtVerify( 248 | jwt, 249 | getKeyFnGetter(jwksUri), 250 | { clockTolerance } 251 | ); 252 | await validate(payload, header, validators); 253 | return { payload, header, token: jwt }; 254 | } catch (e) { 255 | throw new InvalidTokenError(e.message); 256 | } 257 | }; 258 | }; 259 | 260 | export default jwtVerifier; 261 | 262 | export { JWTPayload, JWSHeaderParameters }; 263 | -------------------------------------------------------------------------------- /packages/access-token-jwt/src/validate.ts: -------------------------------------------------------------------------------- 1 | import type { JWTPayload, JWSHeaderParameters } from 'jose'; 2 | 3 | /** 4 | * @ignore 5 | */ 6 | export type FunctionValidator = ( 7 | value: unknown, 8 | claims: JWTPayload, 9 | header: JWSHeaderParameters 10 | ) => Promise | boolean; 11 | 12 | /** 13 | * @ignore 14 | */ 15 | export type Validator = FunctionValidator | string | false | undefined; 16 | 17 | /** 18 | * @ignore 19 | */ 20 | export interface Validators { 21 | alg: Validator; 22 | typ: Validator; 23 | iss: Validator; 24 | aud: Validator; 25 | exp: Validator; 26 | iat: Validator; 27 | sub: Validator; 28 | client_id: Validator; 29 | jti: Validator; 30 | [key: string]: Validator; 31 | } 32 | 33 | export default ( 34 | payload: JWTPayload, 35 | header: JWSHeaderParameters, 36 | validators: Validators 37 | ): Promise => 38 | Promise.all( 39 | Object.entries(validators).map( 40 | async ([key, validator]: [string, Validator]) => { 41 | const value = 42 | key === 'alg' || key === 'typ' ? header[key] : payload[key]; 43 | if ( 44 | validator === false || 45 | (typeof validator === 'string' && value === validator) || 46 | (typeof validator === 'function' && 47 | (await validator(value, payload, header))) 48 | ) { 49 | return; 50 | } 51 | throw new Error(`Unexpected '${key}' value`); 52 | } 53 | ) 54 | ); 55 | 56 | export const defaultValidators = ( 57 | issuer: string, 58 | audience: string | string[], 59 | clockTolerance: number, 60 | maxTokenAge: number | undefined, 61 | strict: boolean, 62 | allowedSigningAlgs: string[] | undefined, 63 | tokenSigningAlg: string | undefined 64 | ): Validators => ({ 65 | alg: (alg) => 66 | typeof alg === 'string' && 67 | alg.toLowerCase() !== 'none' && 68 | (!allowedSigningAlgs || allowedSigningAlgs.includes(alg)) && 69 | (!tokenSigningAlg || alg === tokenSigningAlg), 70 | typ: (typ) => 71 | !strict || 72 | (typeof typ === 'string' && 73 | typ.toLowerCase().replace(/^application\//, '') === 'at+jwt'), 74 | iss: (iss) => iss === issuer, 75 | aud: (aud) => { 76 | audience = typeof audience === 'string' ? [audience] : audience; 77 | if (typeof aud === 'string') { 78 | return audience.includes(aud); 79 | } 80 | if (Array.isArray(aud)) { 81 | return audience.some(Set.prototype.has.bind(new Set(aud))); 82 | } 83 | return false; 84 | }, 85 | exp: (exp) => { 86 | const now = Math.floor(Date.now() / 1000); 87 | return typeof exp === 'number' && exp >= now - clockTolerance; 88 | }, 89 | iat: (iat) => { 90 | if (!maxTokenAge) { 91 | return (iat === undefined && !strict) || typeof iat === 'number'; 92 | } 93 | const now = Math.floor(Date.now() / 1000); 94 | return ( 95 | typeof iat === 'number' && 96 | iat < now + clockTolerance && 97 | iat > now - clockTolerance - maxTokenAge 98 | ); 99 | }, 100 | sub: (sub) => (sub === undefined && !strict) || typeof sub === 'string', 101 | client_id: (clientId) => 102 | (clientId === undefined && !strict) || typeof clientId === 'string', 103 | jti: (jti) => (jti === undefined && !strict) || typeof jti === 'string', 104 | }); 105 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/claim-check.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | claimCheck, 3 | claimEquals, 4 | claimIncludes, 5 | requiredScopes, 6 | scopeIncludesAny, 7 | UnauthorizedError, 8 | InvalidTokenError, 9 | InsufficientScopeError, 10 | } from '../src'; 11 | 12 | describe('claim-check', () => { 13 | describe('claimCheck', () => { 14 | it('should expect a function', () => { 15 | expect(claimCheck).toThrowError("'claimCheck' expects a function"); 16 | }); 17 | 18 | it('should throw for no request', () => { 19 | expect(claimCheck(() => true)).toThrowError(UnauthorizedError); 20 | }); 21 | 22 | it('should throw for an empty payload', () => { 23 | expect(claimCheck(() => true).bind(null)).toThrowError(UnauthorizedError); 24 | }); 25 | 26 | it('should throw when check function returns false', () => { 27 | expect(claimCheck(() => false).bind(null, {})).toThrowError( 28 | InvalidTokenError 29 | ); 30 | }); 31 | 32 | it('should throw with custom error message when check function returns false', () => { 33 | expect(claimCheck(() => false, 'foo').bind(null, {})).toThrowError('foo'); 34 | }); 35 | 36 | it('should not throw when check function returns true', () => { 37 | expect(claimCheck(() => true).bind(null, {})).not.toThrow(); 38 | }); 39 | }); 40 | 41 | describe('claimEquals', () => { 42 | it('should expect a string claim', () => { 43 | expect(claimEquals).toThrowError("'claim' must be a string"); 44 | }); 45 | 46 | it('should expect a JSON primitive claim value', () => { 47 | expect(claimEquals.bind(null, 'claim', {} as string)).toThrowError( 48 | "expected' must be a string, number, boolean or null" 49 | ); 50 | }); 51 | 52 | it('should allow a JSON primitive claim value', () => { 53 | for (const value of ['foo', 1, true, null]) { 54 | expect(claimEquals.bind(null, 'claim', value)).not.toThrow(); 55 | } 56 | }); 57 | 58 | it('should throw if claim not in payload', () => { 59 | expect(claimEquals('foo', 'bar').bind(null, {})).toThrowError( 60 | "Missing 'foo' claim" 61 | ); 62 | }); 63 | 64 | it(`should throw if claim doesn't match expected`, () => { 65 | expect(claimEquals('foo', 'bar').bind(null, { foo: 'baz' })).toThrowError( 66 | "Unexpected 'foo' value" 67 | ); 68 | }); 69 | 70 | it('should not throw if claim matches expected', () => { 71 | expect( 72 | claimEquals('foo', 'bar').bind(null, { foo: 'bar' }) 73 | ).not.toThrow(); 74 | }); 75 | }); 76 | 77 | describe('claimIncludes', () => { 78 | it('should expect a string claim', () => { 79 | expect(claimIncludes).toThrowError("'claim' must be a string"); 80 | }); 81 | 82 | it('should throw if claim not in payload', () => { 83 | expect(claimIncludes('foo', 'bar').bind(null, {})).toThrowError( 84 | "Missing 'foo' claim" 85 | ); 86 | }); 87 | 88 | it('should allow JSON primitive claim values', () => { 89 | expect(claimIncludes.bind(null, 'foo', 'bar', 1, false)).not.toThrow(); 90 | }); 91 | 92 | it('should throw if expected is not found in actual claim string', () => { 93 | expect( 94 | claimIncludes('foo', 'bar', 'baz').bind(null, { 95 | foo: 'qux quxx', 96 | }) 97 | ).toThrowError("Unexpected 'foo' value"); 98 | }); 99 | 100 | it('should throw if all expected are not found in actual claim array', () => { 101 | expect( 102 | claimIncludes('foo', 'bar', 'baz').bind(null, { 103 | foo: ['qux', 'bar'], 104 | }) 105 | ).toThrowError("Unexpected 'foo' value"); 106 | }); 107 | 108 | it('should throw if actual is not array or string', () => { 109 | expect( 110 | claimIncludes('foo', 'bar', 'baz').bind(null, { 111 | foo: true, 112 | }) 113 | ).toThrowError("Unexpected 'foo' value"); 114 | }); 115 | 116 | it('should not throw if all expected found in actual string', () => { 117 | expect( 118 | claimIncludes('foo', 'bar', 'baz').bind(null, { 119 | foo: 'foo bar baz', 120 | }) 121 | ).not.toThrow(); 122 | }); 123 | 124 | it('should not throw if all expected found in actual array', () => { 125 | expect( 126 | claimIncludes('foo', 'bar', 'baz').bind(null, { 127 | foo: ['foo', 'bar', 'baz'], 128 | }) 129 | ).not.toThrow(); 130 | }); 131 | }); 132 | 133 | describe('requiredScopes', () => { 134 | it('should expect a string or array of strings', () => { 135 | expect(requiredScopes).toThrowError( 136 | "scopes' must be a string or array of strings" 137 | ); 138 | }); 139 | 140 | it('should throw if no scope claim found', () => { 141 | expect(requiredScopes('foo bar').bind(null, { foo: 'bar' })).toThrowError( 142 | new InsufficientScopeError(['foo', 'bar'], "Missing 'scope' claim") 143 | ); 144 | }); 145 | 146 | it('should throw if all scopes from string not found in actual', () => { 147 | expect( 148 | requiredScopes('foo bar').bind(null, { scope: 'bar baz' }) 149 | ).toThrowError(new InsufficientScopeError(['foo', 'bar'])); 150 | }); 151 | 152 | it('should throw if all scopes from array not found in actual', () => { 153 | expect( 154 | requiredScopes(['foo', 'bar']).bind(null, { 155 | scope: 'bar baz', 156 | }) 157 | ).toThrowError(new InsufficientScopeError(['foo', 'bar'])); 158 | }); 159 | 160 | it('should not throw if all scopes found in actual', () => { 161 | expect( 162 | requiredScopes(['foo', 'bar']).bind(null, { 163 | scope: 'foo bar', 164 | }) 165 | ).not.toThrow(); 166 | }); 167 | }); 168 | 169 | describe('scopeIncludesAny', () => { 170 | it('should expect a string or array of strings', () => { 171 | expect(scopeIncludesAny).toThrowError( 172 | "scopes' must be a string or array of strings" 173 | ); 174 | }); 175 | 176 | it('should throw if no scope claim found', () => { 177 | expect(scopeIncludesAny('foo bar').bind(null, { foo: 'bar' })).toThrowError( 178 | new InsufficientScopeError(['foo', 'bar'], "Missing 'scope' claim") 179 | ); 180 | }); 181 | 182 | it('should throw if all scopes from string not found in actual', () => { 183 | expect( 184 | scopeIncludesAny('foo bar').bind(null, { scope: 'baz' }) 185 | ).toThrowError(new InsufficientScopeError(['foo', 'bar'])); 186 | }); 187 | 188 | it('should throw if no scopes from array not found in actual', () => { 189 | expect( 190 | scopeIncludesAny(['foo', 'bar']).bind(null, { 191 | scope: 'baz', 192 | }) 193 | ).toThrowError(new InsufficientScopeError(['foo', 'bar'])); 194 | }); 195 | 196 | it('should not throw if all scopes found in actual', () => { 197 | expect( 198 | scopeIncludesAny(['foo', 'bar']).bind(null, { 199 | scope: 'foo bar', 200 | }) 201 | ).not.toThrow(); 202 | }); 203 | 204 | it('should not throw if any scopes found in actual', () => { 205 | expect( 206 | scopeIncludesAny(['foo', 'bar']).bind(null, { 207 | scope: 'foo qux quxx', 208 | }) 209 | ).not.toThrow(); 210 | }); 211 | }) 212 | }); 213 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/discovery.test.ts: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import sinon from 'sinon'; 3 | import { discover as discovery } from '../src'; 4 | 5 | const success = { issuer: 'https://op.example.com' }; 6 | 7 | const discover = ( 8 | uri: string, 9 | timeoutDuration = 5000, 10 | cacheMaxAge = 600000 11 | ) => { 12 | const getDiscovery = discovery({ 13 | issuerBaseURL: uri, 14 | timeoutDuration, 15 | cacheMaxAge, 16 | }); 17 | return getDiscovery(); 18 | }; 19 | 20 | const mins = 60000; 21 | 22 | describe('discover', () => { 23 | afterEach(nock.cleanAll); 24 | 25 | it('gets discovery metadata for custom /.well-known', async () => { 26 | nock('https://op.example.com') 27 | .get('/.well-known/example-configuration') 28 | .reply(200, success); 29 | 30 | await expect( 31 | discover('https://op.example.com/.well-known/example-configuration') 32 | ).resolves.toMatchObject(success); 33 | }); 34 | 35 | it('gets discovery metadata for openid-configuration', async () => { 36 | nock('https://op.example.com') 37 | .get('/.well-known/openid-configuration') 38 | .reply(200, success); 39 | 40 | await expect( 41 | discover('https://op.example.com/.well-known/openid-configuration') 42 | ).resolves.toMatchObject(success); 43 | }); 44 | 45 | it('gets discovery metadata for openid-configuration with base url', async () => { 46 | nock('https://op.example.com') 47 | .get('/.well-known/openid-configuration') 48 | .reply(200, success); 49 | 50 | await expect(discover('https://op.example.com')).resolves.toMatchObject( 51 | success 52 | ); 53 | }); 54 | 55 | it('gets discovery metadata for openid-configuration with path (with trailing slash)', async () => { 56 | nock('https://op.example.com') 57 | .get('/oidc/.well-known/openid-configuration') 58 | .reply(200, { 59 | issuer: 'https://op.example.com/oidc', 60 | }); 61 | 62 | await expect( 63 | discover('https://op.example.com/oidc/') 64 | ).resolves.toMatchObject({ 65 | issuer: 'https://op.example.com/oidc', 66 | }); 67 | }); 68 | 69 | it('gets discovery metadata for openid-configuration with path (without trailing slash)', async () => { 70 | nock('https://op.example.com') 71 | .get('/oidc/.well-known/openid-configuration') 72 | .reply(200, { 73 | issuer: 'https://op.example.com/oidc', 74 | }); 75 | 76 | await expect( 77 | discover('https://op.example.com/oidc') 78 | ).resolves.toMatchObject({ 79 | issuer: 'https://op.example.com/oidc', 80 | }); 81 | }); 82 | 83 | it('gets discovery metadata for openid-configuration with path and query', async () => { 84 | nock('https://op.example.com') 85 | .get('/oidc/.well-known/openid-configuration') 86 | .query({ foo: 'bar' }) 87 | .reply(200, { 88 | issuer: 'https://op.example.com/oidc', 89 | }); 90 | 91 | await expect( 92 | discover( 93 | 'https://op.example.com/oidc/.well-known/openid-configuration?foo=bar' 94 | ) 95 | ).resolves.toMatchObject({ 96 | issuer: 'https://op.example.com/oidc', 97 | }); 98 | }); 99 | 100 | it('gets discovery metadata for oauth-authorization-server', async () => { 101 | nock('https://op.example.com') 102 | .get('/.well-known/oauth-authorization-server') 103 | .reply(200, success); 104 | 105 | await expect( 106 | discover('https://op.example.com/.well-known/oauth-authorization-server') 107 | ).resolves.toMatchObject(success); 108 | }); 109 | 110 | it('gets discovery metadata for oauth-authorization-server with base url', async () => { 111 | nock('https://op.example.com') 112 | .get('/.well-known/oauth-authorization-server') 113 | .reply(200, success); 114 | 115 | await expect(discover('https://op.example.com')).resolves.toMatchObject( 116 | success 117 | ); 118 | }); 119 | 120 | it('gets discovery metadata for oauth-authorization-server with path (with trailing slash)', async () => { 121 | nock('https://op.example.com') 122 | .get('/.well-known/oauth-authorization-server/oauth2/') 123 | .reply(200, success); 124 | 125 | await expect( 126 | discover('https://op.example.com/oauth2/') 127 | ).resolves.toMatchObject(success); 128 | }); 129 | 130 | it('gets discovery metadata for oauth-authorization-server with path (without trailing slash)', async () => { 131 | nock('https://op.example.com') 132 | .get('/.well-known/oauth-authorization-server/oauth2') 133 | .reply(200, success); 134 | 135 | await expect( 136 | discover('https://op.example.com/oauth2') 137 | ).resolves.toMatchObject(success); 138 | }); 139 | 140 | it('gets discovery metadata for oauth-authorization-server with path and query', async () => { 141 | nock('https://op.example.com') 142 | .get('/.well-known/oauth-authorization-server/oauth2') 143 | .query({ foo: 'bar' }) 144 | .reply(200, success); 145 | 146 | await expect( 147 | discover( 148 | 'https://op.example.com/.well-known/oauth-authorization-server/oauth2?foo=bar' 149 | ) 150 | ).resolves.toMatchObject(success); 151 | }); 152 | 153 | it('is rejected when both Metadata endpoints fail', async () => { 154 | nock('https://op.example.com') 155 | .get('/.well-known/openid-configuration') 156 | .reply(500) 157 | .get('/.well-known/oauth-authorization-server') 158 | .reply(200, ''); 159 | 160 | await expect(discover('https://op.example.com')).rejects.toThrowError( 161 | 'Failed to fetch authorization server metadata' 162 | ); 163 | }); 164 | 165 | it('is rejected when .well-known Metadata endpoint fails', async () => { 166 | nock('https://op.example.com') 167 | .get('/.well-known/example-configuration') 168 | .reply(500); 169 | 170 | await expect( 171 | discover('https://op.example.com/.well-known/example-configuration') 172 | ).rejects.toThrowError( 173 | 'Failed to fetch https://op.example.com/.well-known/example-configuration, responded with 500' 174 | ); 175 | }); 176 | 177 | it('is rejected when .well-known Metadata endpoint responds with non JSON response', async () => { 178 | nock('https://op.example.com') 179 | .get('/.well-known/example-configuration') 180 | .reply(200, ''); 181 | 182 | await expect( 183 | discover('https://op.example.com/.well-known/example-configuration') 184 | ).rejects.toThrowError( 185 | 'Failed to parse the response from https://op.example.com/.well-known/example-configuration' 186 | ); 187 | }); 188 | 189 | it('is rejected with Error when no absolute URL is provided', async () => { 190 | await expect( 191 | discover('op.example.com/.well-known/foobar') 192 | ).rejects.toThrowError('Invalid URL'); 193 | }); 194 | 195 | it('is rejected when .well-known Metadata does not provide the required "issuer" property', async () => { 196 | nock('https://op.example.com') 197 | .get('/.well-known/openid-configuration') 198 | .reply(200, {}); 199 | 200 | await expect( 201 | discover('https://op.example.com/.well-known/openid-configuration') 202 | ).rejects.toThrowError( 203 | "'issuer' not found in authorization server metadata" 204 | ); 205 | }); 206 | 207 | it('should cache discovery calls', async function () { 208 | const spy = jest.fn(() => success); 209 | nock('https://op.example.com') 210 | .persist() 211 | .get('/.well-known/openid-configuration') 212 | .reply(200, spy); 213 | 214 | const getDiscovery = discovery({ 215 | issuerBaseURL: 'https://op.example.com/.well-known/openid-configuration', 216 | timeoutDuration: 5000, 217 | cacheMaxAge: 600000, 218 | }); 219 | 220 | await getDiscovery(); 221 | await getDiscovery(); 222 | await getDiscovery(); 223 | expect(spy).toHaveBeenCalledTimes(1); 224 | }); 225 | 226 | it('should handle concurrent discovery calls', async function () { 227 | const spy = jest.fn(() => success); 228 | nock('https://op.example.com') 229 | .persist() 230 | .get('/.well-known/openid-configuration') 231 | .reply(200, spy); 232 | 233 | const getDiscovery = discovery({ 234 | issuerBaseURL: 'https://op.example.com/.well-known/openid-configuration', 235 | timeoutDuration: 5000, 236 | cacheMaxAge: 10 * mins, 237 | }); 238 | 239 | await Promise.all([getDiscovery(), getDiscovery(), getDiscovery()]); 240 | expect(spy).toHaveBeenCalledTimes(1); 241 | }); 242 | 243 | it('should make new calls after max age', async function () { 244 | const clock = sinon.useFakeTimers({ 245 | toFake: ['Date'], 246 | }); 247 | const spy = jest.fn(() => success); 248 | nock('https://op.example.com') 249 | .persist() 250 | .get('/.well-known/openid-configuration') 251 | .reply(200, spy); 252 | 253 | const getDiscovery = discovery({ 254 | issuerBaseURL: 'https://op.example.com/.well-known/openid-configuration', 255 | timeoutDuration: 5000, 256 | cacheMaxAge: 10 * mins, 257 | }); 258 | 259 | await getDiscovery(); 260 | expect(spy).toHaveBeenCalledTimes(1); 261 | clock.tick(5 * mins); 262 | await getDiscovery(); 263 | expect(spy).toHaveBeenCalledTimes(1); 264 | 265 | clock.tick(10 * mins); 266 | await getDiscovery(); 267 | expect(spy).toHaveBeenCalledTimes(2); 268 | 269 | clock.restore(); 270 | }); 271 | 272 | it('should not cache failed discovery calls', async function () { 273 | nock('https://op.example.com') 274 | .get('/.well-known/openid-configuration') 275 | .reply(500); 276 | nock('https://op.example.com') 277 | .get('/.well-known/openid-configuration') 278 | .reply(200, () => success); 279 | 280 | const getDiscovery = discovery({ 281 | issuerBaseURL: 'https://op.example.com/.well-known/openid-configuration', 282 | timeoutDuration: 5000, 283 | cacheMaxAge: 600000, 284 | }); 285 | 286 | await expect(getDiscovery()).rejects.toThrowError(); 287 | await expect(getDiscovery()).resolves.toMatchObject(success); 288 | }); 289 | 290 | it('should handle concurrent client calls with failures', async function () { 291 | nock('https://op.example.com') 292 | .get('/.well-known/openid-configuration') 293 | .reply(500); 294 | nock('https://op.example.com') 295 | .get('/.well-known/openid-configuration') 296 | .reply(200, () => success); 297 | 298 | const getDiscovery = discovery({ 299 | issuerBaseURL: 'https://op.example.com/.well-known/openid-configuration', 300 | timeoutDuration: 5000, 301 | cacheMaxAge: 600000, 302 | }); 303 | 304 | await Promise.all([ 305 | expect(getDiscovery()).rejects.toThrowError(), 306 | expect(getDiscovery()).rejects.toThrowError(), 307 | expect(getDiscovery()).rejects.toThrowError(), 308 | ]); 309 | await expect(getDiscovery()).resolves.toMatchObject(success); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/get-key-fn.test.ts: -------------------------------------------------------------------------------- 1 | import { exportJWK, generateKeyPair } from 'jose'; 2 | import nock from 'nock'; 3 | import getKeyFn from '../src/get-key-fn'; 4 | 5 | describe('get-key-fn', () => { 6 | afterEach(nock.cleanAll); 7 | 8 | it('return a secret key if one is provided', async () => { 9 | const keyFn = getKeyFn({ 10 | secret: 'shhh!', 11 | cooldownDuration: 1000, 12 | timeoutDuration: 1000, 13 | cacheMaxAge: 1000, 14 | }); 15 | const key = await keyFn('foo')(); 16 | expect(key.type).toBe('secret'); 17 | }); 18 | 19 | it('return a JWKS if no secret is provided', async () => { 20 | const { publicKey } = await generateKeyPair('RS256'); 21 | const publicJwk = await exportJWK(publicKey); 22 | nock('https://issuer.example.com/') 23 | .persist() 24 | .get('/jwks.json') 25 | .reply(200, { keys: [{ kid: 'kid', ...publicJwk }] }); 26 | const keyFn = getKeyFn({ 27 | cooldownDuration: 1000, 28 | timeoutDuration: 1000, 29 | cacheMaxAge: 1000, 30 | }); 31 | const key = await keyFn('https://issuer.example.com/jwks.json')({ 32 | alg: 'RS256', 33 | kid: 'kid', 34 | }); 35 | expect(key.type).toBe('public'); 36 | }); 37 | 38 | it('should cache the JWKS provider', async () => { 39 | const keyFn = getKeyFn({ 40 | cooldownDuration: 1000, 41 | timeoutDuration: 1000, 42 | cacheMaxAge: 1000, 43 | }); 44 | const uri = 'https://issuer.example.com/jwks.json'; 45 | const getKeyA = keyFn(uri); 46 | const getKeyB = keyFn(uri); 47 | expect(getKeyA).toBe(getKeyB); 48 | }); 49 | 50 | it('should invalidate JWKS provider cache if jwksUri changes', async () => { 51 | const keyFn = getKeyFn({ 52 | cooldownDuration: 1000, 53 | timeoutDuration: 1000, 54 | cacheMaxAge: 1000, 55 | }); 56 | const getKeyA = keyFn('https://issuer.example.com/jwks1.json'); 57 | const getKeyB = keyFn('https://issuer.example.com/jwks2.json'); 58 | const getKeyC = keyFn('https://issuer.example.com/jwks2.json'); 59 | expect(getKeyA).not.toBe(getKeyB); 60 | expect(getKeyB).toBe(getKeyC); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'buffer'; 2 | import { createSecretKey } from 'crypto'; 3 | import { SignJWT, generateKeyPair, exportJWK } from 'jose'; 4 | import nock = require('nock'); 5 | 6 | export const now = (Date.now() / 1000) | 0; 7 | const day = 60 * 60 * 24; 8 | 9 | interface CreateJWTOptions { 10 | payload?: { [key: string]: any }; 11 | issuer?: string; 12 | subject?: string; 13 | audience?: string; 14 | jwksUri?: string; 15 | discoveryUri?: string; 16 | kid?: string; 17 | iat?: number; 18 | exp?: number; 19 | jwksSpy?: jest.Mock; 20 | discoverSpy?: jest.Mock; 21 | delay?: number; 22 | secret?: string; 23 | } 24 | 25 | export const createJwt = async ({ 26 | payload = {}, 27 | issuer = 'https://issuer.example.com/', 28 | subject = 'me', 29 | audience = 'https://api/', 30 | jwksUri = '/.well-known/jwks.json', 31 | discoveryUri = '/.well-known/openid-configuration', 32 | iat = now, 33 | exp = now + day, 34 | kid = 'kid', 35 | jwksSpy = jest.fn(), 36 | discoverSpy = jest.fn(), 37 | secret, 38 | }: CreateJWTOptions = {}): Promise => { 39 | const { publicKey, privateKey } = await generateKeyPair('RS256'); 40 | const publicJwk = await exportJWK(publicKey); 41 | nock(issuer) 42 | .persist() 43 | .get(jwksUri) 44 | .reply(200, (...args) => { 45 | jwksSpy(...args); 46 | return { keys: [{ kid, ...publicJwk }] }; 47 | }) 48 | .get(discoveryUri) 49 | .reply(200, (...args) => { 50 | discoverSpy(...args); 51 | return { 52 | issuer, 53 | jwks_uri: (issuer + jwksUri).replace('//.well-known', '/.well-known'), 54 | }; 55 | }); 56 | 57 | const secretKey = secret && createSecretKey(Buffer.from(secret)); 58 | 59 | return new SignJWT(payload) 60 | .setProtectedHeader({ 61 | alg: secretKey ? 'HS256' : 'RS256', 62 | typ: 'JWT', 63 | kid, 64 | }) 65 | .setIssuer(issuer) 66 | .setSubject(subject) 67 | .setAudience(audience) 68 | .setIssuedAt(iat) 69 | .setExpirationTime(exp) 70 | .sign(secretKey || privateKey); 71 | }; 72 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { Agent } from 'http'; 3 | import nock = require('nock'); 4 | import sinon = require('sinon'); 5 | import { createJwt } from './helpers'; 6 | import { jwtVerifier } from '../src'; 7 | 8 | describe('index', () => { 9 | afterEach(nock.cleanAll); 10 | 11 | it('gets metadata and verifies jwt with discovery', async () => { 12 | const jwt = await createJwt({ issuer: 'https://op.example.com' }); 13 | 14 | const verify = jwtVerifier({ 15 | issuerBaseURL: 'https://op.example.com', 16 | audience: 'https://api/', 17 | }); 18 | await expect(verify(jwt)).resolves.toHaveProperty('payload', { 19 | iss: 'https://op.example.com', 20 | sub: 'me', 21 | aud: 'https://api/', 22 | iat: expect.any(Number), 23 | exp: expect.any(Number), 24 | }); 25 | }); 26 | 27 | it('gets metadata and verifies jwt without discovery', async () => { 28 | const jwt = await createJwt({ issuer: 'https://op.example.com' }); 29 | 30 | const verify = jwtVerifier({ 31 | issuer: 'https://op.example.com', 32 | jwksUri: 'https://op.example.com/.well-known/jwks.json', 33 | audience: 'https://api/', 34 | }); 35 | await expect(verify(jwt)).resolves.toHaveProperty('payload', { 36 | iss: 'https://op.example.com', 37 | sub: 'me', 38 | aud: 'https://api/', 39 | iat: expect.any(Number), 40 | exp: expect.any(Number), 41 | }); 42 | }); 43 | 44 | it('verifies jwt signed with symmetric secret', async () => { 45 | const secret = randomBytes(32).toString('hex'); 46 | const jwt = await createJwt({ 47 | issuer: 'https://op.example.com', 48 | secret, 49 | }); 50 | 51 | const verify = jwtVerifier({ 52 | issuer: 'https://op.example.com', 53 | secret, 54 | tokenSigningAlg: 'HS256', 55 | audience: 'https://api/', 56 | }); 57 | await expect(verify(jwt)).resolves.toHaveProperty('payload', { 58 | iss: 'https://op.example.com', 59 | sub: 'me', 60 | aud: 'https://api/', 61 | iat: expect.any(Number), 62 | exp: expect.any(Number), 63 | }); 64 | }); 65 | 66 | it('caches discovery and jwks requests in memory', async () => { 67 | const discoverSpy = jest.fn(); 68 | const jwksSpy = jest.fn(); 69 | 70 | const jwt = await createJwt({ 71 | issuer: 'https://op.example.com', 72 | jwksSpy, 73 | discoverSpy, 74 | }); 75 | 76 | const verify = jwtVerifier({ 77 | issuerBaseURL: 'https://op.example.com', 78 | audience: 'https://api/', 79 | }); 80 | await expect(verify(jwt)).resolves.toBeTruthy(); 81 | await expect(verify(jwt)).resolves.toBeTruthy(); 82 | await expect(verify(jwt)).resolves.toBeTruthy(); 83 | expect(discoverSpy).toHaveBeenCalledTimes(1); 84 | expect(jwksSpy).toHaveBeenCalledTimes(1); 85 | }); 86 | 87 | it('caches discovery in memory once', async () => { 88 | const discoverSpy = jest.fn(); 89 | 90 | const jwt = await createJwt({ 91 | issuer: 'https://op.example.com', 92 | discoverSpy, 93 | }); 94 | 95 | const verify = jwtVerifier({ 96 | issuerBaseURL: 'https://op.example.com', 97 | audience: 'https://api/', 98 | }); 99 | await expect( 100 | Promise.all([verify(jwt), verify(jwt), verify(jwt)]) 101 | ).resolves.toBeTruthy(); 102 | expect(discoverSpy).toHaveBeenCalledTimes(1); 103 | }); 104 | 105 | it('handles rotated signing keys', async () => { 106 | // @FIXME Use jest timers when facebook/jest#10221 is fixed 107 | const clock = sinon.useFakeTimers(); 108 | const jwksSpy = jest.fn(); 109 | const oldJwt = await createJwt({ 110 | issuer: 'https://op.example.com', 111 | jwksSpy, 112 | kid: 'a', 113 | }); 114 | 115 | const verify = jwtVerifier({ 116 | issuer: 'https://op.example.com', 117 | jwksUri: 'https://op.example.com/.well-known/jwks.json', 118 | audience: 'https://api/', 119 | cooldownDuration: 1000, 120 | }); 121 | await expect(verify(oldJwt)).resolves.toBeTruthy(); 122 | 123 | nock.cleanAll(); 124 | const newJwt = await createJwt({ 125 | issuer: 'https://op.example.com', 126 | jwksSpy, 127 | kid: 'b', 128 | }); 129 | clock.tick(1000); 130 | 131 | await expect(verify(newJwt)).resolves.toBeTruthy(); 132 | clock.restore(); 133 | }); 134 | 135 | it('should accept custom http options', async () => { 136 | const jwt = await createJwt({ 137 | issuer: 'https://op.example.com', 138 | }); 139 | 140 | const verify = jwtVerifier({ 141 | issuerBaseURL: 'https://op.example.com', 142 | audience: 'https://api/', 143 | agent: new Agent(), 144 | timeoutDuration: 1000, 145 | }); 146 | const promise = verify(jwt); 147 | await expect(promise).resolves.toBeTruthy(); 148 | }); 149 | 150 | it('should accept custom validators', async () => { 151 | const jwt = await createJwt({ 152 | issuer: 'https://op.example.com', 153 | payload: { foo: 'baz' }, 154 | }); 155 | 156 | const verify = jwtVerifier({ 157 | issuerBaseURL: 'https://op.example.com', 158 | audience: 'https://api/', 159 | agent: new Agent(), 160 | timeoutDuration: 1000, 161 | validators: { 162 | foo: 'bar', 163 | }, 164 | }); 165 | const promise = verify(jwt); 166 | await expect(promise).rejects.toThrow(`Unexpected 'foo' value`); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/jwt-verifier.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import nock from 'nock'; 3 | import sinon from 'sinon'; 4 | import { createJwt, now } from './helpers'; 5 | import { jwtVerifier, InvalidTokenError } from '../src'; 6 | import validate from '../src/validate.js'; 7 | 8 | describe('jwt-verifier', () => { 9 | afterEach(nock.cleanAll); 10 | 11 | it('should throw when configured with no jwksUri or issuerBaseURL and issuer', async () => { 12 | expect(() => 13 | jwtVerifier({ 14 | audience: 'https://api/', 15 | }) 16 | ).toThrowError( 17 | "You must provide an 'issuerBaseURL', an 'issuer' and 'jwksUri' or an 'issuer' and 'secret'" 18 | ); 19 | }); 20 | 21 | it('should throw when configured with no audience', async () => { 22 | expect(() => 23 | jwtVerifier({ 24 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 25 | issuer: 'https://issuer.example.com/', 26 | }) 27 | ).toThrowError("An 'audience' is required to validate the 'aud' claim"); 28 | }); 29 | 30 | it('should throw when configured with secret and no token signing alg', async () => { 31 | expect(() => 32 | jwtVerifier({ 33 | issuer: 'https://issuer.example.com/', 34 | audience: 'https://api/', 35 | secret: randomBytes(32).toString('hex'), 36 | }) 37 | ).toThrowError( 38 | "You must provide a 'tokenSigningAlg' for validating symmetric algorithms" 39 | ); 40 | }); 41 | 42 | it('should throw when configured with secret and invalid token signing alg', async () => { 43 | expect(() => 44 | jwtVerifier({ 45 | issuer: 'https://issuer.example.com/', 46 | audience: 'https://api/', 47 | secret: randomBytes(32).toString('hex'), 48 | tokenSigningAlg: 'none', 49 | }) 50 | ).toThrowError( 51 | "You must supply one of HS256, HS384, HS512 for 'tokenSigningAlg' to validate symmetrically signed tokens" 52 | ); 53 | }); 54 | 55 | it('should throw when configured with JWKS uri and invalid token signing alg', async () => { 56 | expect(() => 57 | jwtVerifier({ 58 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 59 | issuer: 'https://issuer.example.com/', 60 | audience: 'https://api/', 61 | tokenSigningAlg: 'none', 62 | }) 63 | ).toThrowError( 64 | "You must supply one of RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES256K, ES384, ES512, EdDSA for 'tokenSigningAlg' to validate asymmetrically signed tokens" 65 | ); 66 | }); 67 | 68 | it('should verify the token', async () => { 69 | const jwt = await createJwt(); 70 | 71 | const verify = jwtVerifier({ 72 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 73 | issuer: 'https://issuer.example.com/', 74 | audience: 'https://api/', 75 | }); 76 | await expect(verify(jwt)).resolves.toHaveProperty('payload', { 77 | iss: 'https://issuer.example.com/', 78 | sub: 'me', 79 | aud: 'https://api/', 80 | iat: expect.any(Number), 81 | exp: expect.any(Number), 82 | }); 83 | }); 84 | 85 | it('should throw for unexpected issuer', async () => { 86 | const jwt = await createJwt({ 87 | issuer: 'https://issuer1.example.com/', 88 | }); 89 | 90 | const verify = jwtVerifier({ 91 | jwksUri: 'https://issuer1.example.com/.well-known/jwks.json', 92 | issuer: 'https://issuer2.example.com/', 93 | audience: 'https://api/', 94 | }); 95 | await expect(verify(jwt)).rejects.toThrowError(`Unexpected 'iss' value`); 96 | }); 97 | 98 | it('should throw for unexpected audience', async () => { 99 | const jwt = await createJwt({ 100 | audience: 'https://api1/', 101 | }); 102 | 103 | const verify = jwtVerifier({ 104 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 105 | issuer: 'https://issuer.example.com/', 106 | audience: 'https://api2/', 107 | }); 108 | await expect(verify(jwt)).rejects.toThrowError(`Unexpected 'aud' value`); 109 | }); 110 | 111 | it('should throw for an expired token', async () => { 112 | const jwt = await createJwt({ 113 | exp: now - 10, 114 | }); 115 | 116 | const verify = jwtVerifier({ 117 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 118 | issuer: 'https://issuer.example.com/', 119 | audience: 'https://api/', 120 | }); 121 | await expect(verify(jwt)).rejects.toThrowError( 122 | '"exp" claim timestamp check failed' 123 | ); 124 | }); 125 | 126 | it('should throw for invalid nbf', async () => { 127 | const clock = sinon.useFakeTimers(1000); 128 | const jwt = await createJwt({ 129 | payload: { 130 | nbf: 2000, 131 | }, 132 | }); 133 | 134 | const verify = jwtVerifier({ 135 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 136 | issuer: 'https://issuer.example.com/', 137 | audience: 'https://api/', 138 | }); 139 | await expect(verify(jwt)).rejects.toThrowError( 140 | '"nbf" claim timestamp check failed' 141 | ); 142 | clock.restore(); 143 | }); 144 | 145 | it('should validate nbf claim with clockTolerance', async () => { 146 | const clock = sinon.useFakeTimers(1000); 147 | const jwt = await createJwt({ 148 | payload: { 149 | nbf: 2000, 150 | }, 151 | }); 152 | 153 | const verify = jwtVerifier({ 154 | jwksUri: 'https://issuer.example.com/.well-known/jwks.json', 155 | issuer: 'https://issuer.example.com/', 156 | audience: 'https://api/', 157 | clockTolerance: 5000, 158 | }); 159 | await expect(verify(jwt)).resolves.not.toThrow(); 160 | clock.restore(); 161 | }); 162 | 163 | it('should throw unexpected token signing alg', async () => { 164 | const secret = randomBytes(32).toString('hex'); 165 | const jwt = await createJwt({ secret }); 166 | 167 | const verify = jwtVerifier({ 168 | secret, 169 | issuer: 'https://issuer.example.com/', 170 | audience: 'https://api/', 171 | tokenSigningAlg: 'HS384', 172 | }); 173 | await expect(verify(jwt)).rejects.toThrowError("Unexpected 'alg' value"); 174 | }); 175 | 176 | it('should fail with invalid_token error', async () => { 177 | const jwt = await createJwt({ 178 | issuer: 'https://issuer.example.com', 179 | }); 180 | const verify = jwtVerifier({ 181 | issuerBaseURL: 182 | 'https://issuer.example.com/.well-known/openid-configuration', 183 | audience: 'https://api/', 184 | }); 185 | await expect(verify(`CORRUPT-${jwt}`)).rejects.toThrow(InvalidTokenError); 186 | }); 187 | 188 | it('should honor configured cache max age', async () => { 189 | const clock = sinon.useFakeTimers({ 190 | toFake: ['Date'], 191 | }); 192 | const jwksSpy = jest.fn(); 193 | const discoverSpy = jest.fn(); 194 | const jwt = await createJwt({ 195 | jwksSpy, 196 | discoverSpy, 197 | }); 198 | 199 | const verify = jwtVerifier({ 200 | issuerBaseURL: 'https://issuer.example.com/', 201 | audience: 'https://api/', 202 | cacheMaxAge: 10, 203 | }); 204 | await expect(verify(jwt)).resolves.toHaveProperty('payload'); 205 | await expect(verify(jwt)).resolves.toHaveProperty('payload'); 206 | expect(jwksSpy).toHaveBeenCalledTimes(1); 207 | expect(discoverSpy).toHaveBeenCalledTimes(1); 208 | clock.tick(11); 209 | await expect(verify(jwt)).resolves.toHaveProperty('payload'); 210 | expect(jwksSpy).toHaveBeenCalledTimes(2); 211 | expect(discoverSpy).toHaveBeenCalledTimes(2); 212 | clock.restore(); 213 | }); 214 | 215 | it('should not cache failed requests', async () => { 216 | nock('https://issuer.example.com/') 217 | .get('/.well-known/openid-configuration') 218 | .reply(500) 219 | .get('/.well-known/jwks.json') 220 | .reply(500); 221 | 222 | const jwt = await createJwt(); 223 | 224 | const verify = jwtVerifier({ 225 | issuerBaseURL: 'https://issuer.example.com/', 226 | audience: 'https://api/', 227 | cacheMaxAge: 10, 228 | }); 229 | await expect(verify(jwt)).rejects.toThrowError( 230 | /Failed to fetch authorization server metadata/ 231 | ); 232 | await expect(verify(jwt)).rejects.toThrowError( 233 | /Expected 200 OK from the JSON Web Key Set HTTP response/ 234 | ); 235 | await expect(verify(jwt)).resolves.toHaveProperty('payload'); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /packages/access-token-jwt/test/validate.test.ts: -------------------------------------------------------------------------------- 1 | import sinon = require('sinon'); 2 | import validate, { defaultValidators } from '../src/validate'; 3 | 4 | const header = { 5 | alg: 'RS256', 6 | typ: 'JWT', 7 | }; 8 | const payload = { 9 | iss: 'https://issuer.example.com', 10 | aud: 'http://api.example.com', 11 | exp: ((Date.now() / 1000) | 0) + 60 * 60, 12 | iat: (Date.now() / 1000) | 0, 13 | sub: 'foo', 14 | client_id: 'bar', 15 | jti: 'baz', 16 | }; 17 | 18 | const validators = ({ 19 | issuer = 'https://issuer.example.com', 20 | audience = 'http://api.example.com', 21 | clockTolerance = 10, 22 | maxTokenAge = 10, 23 | strict = false, 24 | allowedSigningAlgs, 25 | tokenSigningAlg, 26 | }: { 27 | issuer?: string; 28 | audience?: string | string[]; 29 | clockTolerance?: number; 30 | maxTokenAge?: number; 31 | strict?: boolean; 32 | allowedSigningAlgs?: string[]; 33 | tokenSigningAlg?: string; 34 | } = {}) => 35 | defaultValidators( 36 | issuer, 37 | audience, 38 | clockTolerance, 39 | maxTokenAge, 40 | strict, 41 | allowedSigningAlgs, 42 | tokenSigningAlg 43 | ); 44 | 45 | describe('validate', () => { 46 | it('should validate a jwt with default validators', async () => { 47 | await expect( 48 | validate(payload, header, validators()) 49 | ).resolves.not.toThrow(); 50 | }); 51 | it('should throw for invalid alg header', async () => { 52 | await expect( 53 | validate(payload, { ...header, alg: 'none' }, validators()) 54 | ).rejects.toThrow(`Unexpected 'alg' value`); 55 | await expect( 56 | validate(payload, { ...header, alg: 'None' }, validators()) 57 | ).rejects.toThrow(`Unexpected 'alg' value`); 58 | await expect( 59 | validate(payload, { ...header, alg: 'NONE' }, validators()) 60 | ).rejects.toThrow(`Unexpected 'alg' value`); 61 | await expect( 62 | validate( 63 | payload, 64 | { ...header, alg: 'RS256' }, 65 | validators({ allowedSigningAlgs: ['HS256'] }) 66 | ) 67 | ).rejects.toThrow(`Unexpected 'alg' value`); 68 | }); 69 | it('should disable alg header check', async () => { 70 | await expect( 71 | validate( 72 | payload, 73 | { ...header, alg: 'none' }, 74 | { ...validators(), alg: false } 75 | ) 76 | ).resolves.not.toThrow(); 77 | }); 78 | it('should throw for invalid typ in strict mode', async () => { 79 | await expect( 80 | validate(payload, header, validators({ strict: true })) 81 | ).rejects.toThrow(`Unexpected 'typ' value`); 82 | await expect( 83 | validate( 84 | payload, 85 | { ...header, typ: 1 as any }, 86 | validators({ strict: true }) 87 | ) 88 | ).rejects.toThrow(`Unexpected 'typ' value`); 89 | }); 90 | it('should validate typ in strict mode', async () => { 91 | await expect( 92 | validate( 93 | payload, 94 | { ...header, typ: 'at+jwt' }, 95 | validators({ strict: true }) 96 | ) 97 | ).resolves.not.toThrow(); 98 | await expect( 99 | validate( 100 | payload, 101 | { ...header, typ: 'AT+JWT' }, 102 | validators({ strict: true }) 103 | ) 104 | ).resolves.not.toThrow(); 105 | await expect( 106 | validate( 107 | payload, 108 | { ...header, typ: 'application/at+jwt' }, 109 | validators({ strict: true }) 110 | ) 111 | ).resolves.not.toThrow(); 112 | }); 113 | it('should throw for missing claims in strict mode', async () => { 114 | await expect( 115 | validate( 116 | { ...payload, sub: undefined }, 117 | { ...header, typ: 'at+jwt' }, 118 | validators({ strict: true }) 119 | ) 120 | ).rejects.toThrow(`Unexpected 'sub' value`); 121 | await expect( 122 | validate( 123 | { ...payload, client_id: undefined }, 124 | { ...header, typ: 'at+jwt' }, 125 | validators({ strict: true }) 126 | ) 127 | ).rejects.toThrow(`Unexpected 'client_id' value`); 128 | await expect( 129 | validate( 130 | { ...payload, jti: undefined }, 131 | { ...header, typ: 'at+jwt' }, 132 | validators({ strict: true }) 133 | ) 134 | ).rejects.toThrow(`Unexpected 'jti' value`); 135 | }); 136 | it('should throw for invalid claims in strict mode', async () => { 137 | await expect( 138 | validate( 139 | { ...payload, sub: 1 as any }, 140 | { ...header, typ: 'at+jwt' }, 141 | validators({ strict: true }) 142 | ) 143 | ).rejects.toThrow(`Unexpected 'sub' value`); 144 | await expect( 145 | validate( 146 | { ...payload, client_id: ['bar'] }, 147 | { ...header, typ: 'at+jwt' }, 148 | validators({ strict: true }) 149 | ) 150 | ).rejects.toThrow(`Unexpected 'client_id' value`); 151 | await expect( 152 | validate( 153 | { ...payload, jti: true as any }, 154 | { ...header, typ: 'at+jwt' }, 155 | validators({ strict: true }) 156 | ) 157 | ).rejects.toThrow(`Unexpected 'jti' value`); 158 | }); 159 | it('should throw for issuer mismatch', async () => { 160 | await expect( 161 | validate({ ...payload, iss: 'foo' }, header, validators()) 162 | ).rejects.toThrow(`Unexpected 'iss' value`); 163 | }); 164 | it('should throw for audience mismatch', async () => { 165 | await expect( 166 | validate( 167 | { ...payload, aud: 'foo' }, 168 | header, 169 | validators({ audience: ['bar'] }) 170 | ) 171 | ).rejects.toThrow(`Unexpected 'aud' value`); 172 | await expect( 173 | validate( 174 | { ...payload, aud: ['bar'] }, 175 | header, 176 | validators({ audience: ['foo'] }) 177 | ) 178 | ).rejects.toThrow(`Unexpected 'aud' value`); 179 | await expect( 180 | validate( 181 | { ...payload, aud: 1 as any }, 182 | header, 183 | validators({ audience: 'foo' }) 184 | ) 185 | ).rejects.toThrow(`Unexpected 'aud' value`); 186 | }); 187 | it('should validate aud claim', async () => { 188 | await expect( 189 | validate( 190 | { ...payload, aud: 'foo' }, 191 | header, 192 | validators({ audience: ['foo'] }) 193 | ) 194 | ).resolves.not.toThrow(); 195 | await expect( 196 | validate( 197 | { ...payload, aud: ['foo', 'bar'] }, 198 | header, 199 | validators({ audience: ['foo', 'bar', 'baz'] }) 200 | ) 201 | ).resolves.not.toThrow(); 202 | await expect( 203 | validate( 204 | { ...payload, aud: ['foo', 'bar', 'baz'] }, 205 | header, 206 | validators({ audience: ['foo', 'bar'] }) 207 | ) 208 | ).resolves.not.toThrow(); 209 | }); 210 | it('should throw for invalid exp claim', async () => { 211 | const clock = sinon.useFakeTimers(100 * 1000); 212 | await expect( 213 | validate( 214 | { ...payload, exp: 50, iat: 0 }, 215 | header, 216 | validators({ clockTolerance: 0, maxTokenAge: 100 }) 217 | ) 218 | ).rejects.toThrow(`Unexpected 'exp' value`); 219 | await expect( 220 | validate( 221 | { ...payload, exp: 'foo' as any, iat: 0 }, 222 | header, 223 | validators({ clockTolerance: 0, maxTokenAge: 100 }) 224 | ) 225 | ).rejects.toThrow(`Unexpected 'exp' value`); 226 | clock.restore(); 227 | }); 228 | it('should validate exp claim with clockTolerance', async () => { 229 | const clock = sinon.useFakeTimers(100 * 1000); 230 | await expect( 231 | validate( 232 | { ...payload, exp: 50, iat: 0 }, 233 | header, 234 | validators({ clockTolerance: 75, maxTokenAge: 100 }) 235 | ) 236 | ).resolves.not.toThrow(); 237 | clock.restore(); 238 | }); 239 | it('should throw for invalid iat claim', async () => { 240 | const clock = sinon.useFakeTimers(100 * 1000); 241 | await expect( 242 | validate( 243 | { ...payload, iat: 0 }, 244 | header, 245 | validators({ clockTolerance: 0, maxTokenAge: 90 }) 246 | ) 247 | ).rejects.toThrow(`Unexpected 'iat' value`); 248 | await expect( 249 | validate( 250 | { ...payload, iat: undefined }, 251 | { ...header, typ: 'at+jwt' }, 252 | validators({ maxTokenAge: 0, strict: true }) 253 | ) 254 | ).rejects.toThrow(`Unexpected 'iat' value`); 255 | await expect( 256 | validate({ ...payload, iat: 'foo' as any }, header, validators()) 257 | ).rejects.toThrow(`Unexpected 'iat' value`); 258 | await expect( 259 | validate({ ...payload, iat: 200 }, header, validators()) 260 | ).rejects.toThrow(`Unexpected 'iat' value`); 261 | await expect( 262 | validate({ ...payload, iat: 0 }, header, validators()) 263 | ).rejects.toThrow(`Unexpected 'iat' value`); 264 | clock.restore(); 265 | }); 266 | it('should validate iat claim', async () => { 267 | const clock = sinon.useFakeTimers(100 * 1000); 268 | await expect( 269 | validate( 270 | { ...payload, iat: 200 }, 271 | header, 272 | validators({ maxTokenAge: 100 }) 273 | ) 274 | ).rejects.toThrow(`Unexpected 'iat' value`); 275 | await expect( 276 | validate({ ...payload, iat: 0 }, header, validators({ maxTokenAge: 100 })) 277 | ).resolves.not.toThrow(); 278 | await expect( 279 | validate( 280 | { ...payload, iat: 200 }, 281 | header, 282 | validators({ clockTolerance: 100 }) 283 | ) 284 | ).rejects.toThrow(`Unexpected 'iat' value`); 285 | await expect( 286 | validate( 287 | { ...payload, iat: 0 }, 288 | header, 289 | validators({ clockTolerance: 100 }) 290 | ) 291 | ).resolves.not.toThrow(); 292 | clock.restore(); 293 | }); 294 | it('should validate with custom validators', async () => { 295 | await expect( 296 | validate({ ...payload, foo: 'bar' }, header, { 297 | ...validators(), 298 | foo: 'baz', 299 | }) 300 | ).rejects.toThrow(`Unexpected 'foo' value`); 301 | await expect( 302 | validate({ ...payload, foo: 'bar' }, header, { 303 | ...validators(), 304 | foo: 'bar', 305 | }) 306 | ).resolves.not.toThrow(); 307 | await expect( 308 | validate({ ...payload, foo: 'bar' }, header, { 309 | ...validators(), 310 | foo: () => false, 311 | }) 312 | ).rejects.toThrow(`Unexpected 'foo' value`); 313 | await expect( 314 | validate({ ...payload, foo: 'bar' }, header, { 315 | ...validators(), 316 | foo: () => Promise.resolve(true), 317 | }) 318 | ).resolves.not.toThrow(); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /packages/access-token-jwt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "useUnknownInCatchVariables": false, 5 | "allowJs": true, 6 | "removeComments": true, 7 | "module": "ES2020", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "outDir": "./dist" 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } 16 | 17 | -------------------------------------------------------------------------------- /packages/examples/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /packages/examples/express-api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | auth, 3 | requiredScopes, 4 | scopeIncludesAny, 5 | claimEquals, 6 | claimIncludes, 7 | } from 'express-oauth2-jwt-bearer'; 8 | import express = require('express'); 9 | import cors = require('cors'); 10 | import { Handler } from 'express'; 11 | import secret from './secret'; 12 | 13 | const app = express(); 14 | const issuerBaseURL = 'http://localhost:3000'; 15 | const audience = 'https://api'; 16 | const handler: Handler = (req, res) => { 17 | res.json({ msg: 'Hello World!' }); 18 | }; 19 | const requiresAuth = auth({ issuerBaseURL, audience }); 20 | 21 | app.use( 22 | cors({ 23 | origin: issuerBaseURL, 24 | allowedHeaders: ['Authorization'], 25 | exposedHeaders: ['WWW-Authenticate'], 26 | }) 27 | ); 28 | 29 | app.get('/auth', requiresAuth, handler); 30 | 31 | app.get('/scope', requiresAuth, requiredScopes('read:msg'), handler); 32 | 33 | app.get( 34 | '/any-scope', 35 | requiresAuth, 36 | scopeIncludesAny(['read:msg', 'audit:read']), 37 | handler 38 | ); 39 | 40 | app.get('/claim-equals', requiresAuth, claimEquals('foo', 'bar'), handler); 41 | 42 | app.get( 43 | '/claim-includes', 44 | requiresAuth, 45 | claimIncludes('foo', 'bar', 'baz'), 46 | handler 47 | ); 48 | 49 | app.get( 50 | '/custom', 51 | auth({ issuerBaseURL, audience, validators: { iss: false } }), 52 | handler 53 | ); 54 | 55 | app.get('/strict', auth({ issuerBaseURL, audience, strict: true }), handler); 56 | 57 | app.get( 58 | '/symmetric', 59 | auth({ secret, issuer: issuerBaseURL, audience, tokenSigningAlg: 'HS256' }), 60 | handler 61 | ); 62 | 63 | export default app; 64 | -------------------------------------------------------------------------------- /packages/examples/index.ts: -------------------------------------------------------------------------------- 1 | import express from './express-api'; 2 | import playground from './playground'; 3 | 4 | playground.listen(3000, () => 5 | console.log('Playground app at http://localhost:3000') 6 | ); 7 | express.listen(3001, () => console.log('Express API at http://localhost:3001')); 8 | -------------------------------------------------------------------------------- /packages/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Auth0 ", 6 | "version": "0.0.1", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "lint": "exit 0", 11 | "test": "exit 0", 12 | "build": "exit 0", 13 | "dev": "tsnd index.ts" 14 | }, 15 | "devDependencies": { 16 | "@tsconfig/node12": "^1.0.11", 17 | "@types/cors": "^2.8.13", 18 | "@types/node": "^14.18.42", 19 | "@typescript-eslint/eslint-plugin": "^5.57.0", 20 | "@typescript-eslint/parser": "^5.57.0", 21 | "cors": "^2.8.5", 22 | "ejs": "^3.1.9", 23 | "eslint": "^8.37.0", 24 | "express": "^4.18.2", 25 | "got": "^11.8.6", 26 | "jest": "^29.5.0", 27 | "jose": "^4.15.5", 28 | "prettier": "~2.5.1", 29 | "ts-jest": "^29.0.5", 30 | "ts-node-dev": "^2.0.0", 31 | "tslib": "^2.5.0", 32 | "typescript": "^5.0.2" 33 | }, 34 | "engines": { 35 | "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/examples/playground.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 |
21 |
22 |

Header

23 | 30 |

Payload

31 | 43 |

Access Token

44 | 45 |
46 |
47 |

Test:

48 |
49 | Express 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 |

Log:

60 |
61 | 64 | 67 |
68 |
69 | 70 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /packages/examples/playground.ts: -------------------------------------------------------------------------------- 1 | import express = require('express'); 2 | import { generateKeyPair, exportJWK } from 'jose'; 3 | import type { JWK } from 'jose'; 4 | import secret from './secret'; 5 | 6 | const app = express(); 7 | 8 | let publicJwk: JWK; 9 | let privateJwk: JWK; 10 | 11 | const issuer = 'http://localhost:3000'; 12 | const audience = 'https://api'; 13 | 14 | const keys = async () => { 15 | if (publicJwk && privateJwk) { 16 | return { publicJwk, privateJwk }; 17 | } 18 | const { publicKey, privateKey } = await generateKeyPair('RS256'); 19 | publicJwk = await exportJWK(publicKey); 20 | privateJwk = await exportJWK(privateKey); 21 | return { publicJwk, privateJwk }; 22 | }; 23 | 24 | app.set('views', __dirname); 25 | app.set('view engine', 'ejs'); 26 | 27 | app.get('/', async (req, res, next) => { 28 | const { privateJwk } = await keys(); 29 | res.render('playground.ejs', { 30 | privateJwk, 31 | secret, 32 | issuer, 33 | audience, 34 | }); 35 | }); 36 | 37 | app.get('/jwks', async (req, res, next) => { 38 | const { publicJwk } = await keys(); 39 | res.json({ keys: [{ ...publicJwk, alg: 'RS256', kid: '1' }] }); 40 | next(); 41 | }); 42 | 43 | app.get('/.well-known/openid-configuration', async (req, res, next) => { 44 | res.json({ 45 | issuer, 46 | jwks_uri: `${issuer}/jwks`, 47 | }); 48 | next(); 49 | }); 50 | 51 | export default app; 52 | -------------------------------------------------------------------------------- /packages/examples/secret.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | 3 | export default randomBytes(32).toString('hex'); 4 | -------------------------------------------------------------------------------- /packages/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "declaration": true 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-namespace": "off" 14 | }, 15 | "ignorePatterns": ["*.js"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/.shiprc: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | ".version": [] 4 | }, 5 | "postbump": "npm install --prefix=../.. && npm run docs --prefix=../.." 6 | } 7 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/.version: -------------------------------------------------------------------------------- 1 | v1.6.1 -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.6.1](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.6.1) (2025-03-14) 4 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.6.0...v1.6.1) 5 | 6 | **Added** 7 | - Add support for node 22.1.0 [\#167](https://github.com/auth0/node-oauth2-jwt-bearer/pull/167) ([nandan-bhat](https://github.com/nandan-bhat)) 8 | 9 | ## [v1.6.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.6.0) (2023-10-09) 10 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.5.0...v1.6.0) 11 | 12 | **Added** 13 | - nbf claim validation should include clockTolerance [\#115](https://github.com/auth0/node-oauth2-jwt-bearer/pull/115) ([adamjmcgrath](https://github.com/adamjmcgrath)) 14 | 15 | ## [v1.5.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.5.0) (2023-05-24) 16 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.4.1...v1.5.0) 17 | 18 | **Added** 19 | - Add support for Node version 20 [\#108](https://github.com/auth0/node-oauth2-jwt-bearer/pull/108) ([EdenGottlieb](https://github.com/EdenGottlieb)) 20 | 21 | ## [v1.4.1](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.4.1) (2023-04-14) 22 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.4.0...v1.4.1) 23 | 24 | **Fixed** 25 | - fix missing agent [\#103](https://github.com/auth0/node-oauth2-jwt-bearer/pull/103) ([indeed404](https://github.com/indeed404)) 26 | 27 | ## [v1.4.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.4.0) (2023-04-03) 28 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.3.0...v1.4.0) 29 | 30 | **Added** 31 | - Cache max age [\#98](https://github.com/auth0/node-oauth2-jwt-bearer/pull/98) ([adamjmcgrath](https://github.com/adamjmcgrath)) 32 | - Add authRequired option [\#97](https://github.com/auth0/node-oauth2-jwt-bearer/pull/97) ([adamjmcgrath](https://github.com/adamjmcgrath)) 33 | 34 | ## [v1.3.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.3.0) (2022-12-19) 35 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.2.0...v1.3.0) 36 | 37 | **Added** 38 | - Add scopeIncludesAny middleware [\#84](https://github.com/auth0/node-oauth2-jwt-bearer/pull/84) ([ewanharris](https://github.com/ewanharris)) 39 | 40 | ## [v1.2.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.2.0) (2022-10-27) 41 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.1.0...v1.2.0) 42 | 43 | **Added** 44 | - Add 18 LTS to engines [\#80](https://github.com/auth0/node-oauth2-jwt-bearer/pull/80) ([adamjmcgrath](https://github.com/adamjmcgrath)) 45 | 46 | ## [v1.1.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.1.0) (2021-12-15) 47 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.0.1...v1.1.0) 48 | 49 | **Added** 50 | - Export error types [SDK-2941] [\#46](https://github.com/auth0/node-oauth2-jwt-bearer/pull/46) ([adamjmcgrath](https://github.com/adamjmcgrath)) 51 | - Add Node 16 LTS to engines [\#43](https://github.com/auth0/node-oauth2-jwt-bearer/pull/43) ([adamjmcgrath](https://github.com/adamjmcgrath)) 52 | 53 | ## [v1.0.1](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.0.1) (2021-10-25) 54 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v1.0.0...v1.0.1) 55 | 56 | **Fixed** 57 | - Bundle type definitions for monorepo packages [\#39](https://github.com/auth0/node-oauth2-jwt-bearer/pull/39) ([adamjmcgrath](https://github.com/adamjmcgrath)) 58 | 59 | ## [v1.0.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v1.0.0) (2021-10-18) 60 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v0.2.0...v1.0.0) 61 | 62 | **Changed** 63 | - refactor: use jose@4 [\#31](https://github.com/auth0/node-oauth2-jwt-bearer/pull/31) ([panva](https://github.com/panva)) 64 | 65 | **Fixed** 66 | - Throw 403 when missing scope claim altogether [\#35](https://github.com/auth0/node-oauth2-jwt-bearer/pull/35) ([adamjmcgrath](https://github.com/adamjmcgrath)) 67 | 68 | ## [v0.2.0](https://github.com/auth0/node-oauth2-jwt-bearer/tree/v0.2.0) (2021-10-14) 69 | [Full Changelog](https://github.com/auth0/node-oauth2-jwt-bearer/compare/v0.1.0...v0.2.0) 70 | 71 | **⚠️ BREAKING CHANGES** 72 | - [SDK-2827] Update some error status codes and descriptions [\#27](https://github.com/auth0/node-oauth2-jwt-bearer/pull/27) ([adamjmcgrath](https://github.com/adamjmcgrath)) 73 | 74 | ## [0.1.0](https://github.com/auth0/node-oauth2-jwt-bearer/releases/tag/v0.1.0-express) (2021-07-14) 75 | 76 | **Added** 77 | 78 | - Add support for Access Tokens signed with a symmetric secret [#21](https://github.com/auth0/node-oauth2-jwt-bearer/pull/21) ([adamjmcgrath](https://github.com/adamjmcgrath)) 79 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [Restrict access with scopes](#restrict-access-with-scopes) 4 | - [Restrict access with claims](#restrict-access-with-claims) 5 | - [Matching a specific value](#matching-a-specific-value) 6 | - [Matching multiple values](#matching-multiple-values) 7 | - [Matching custom logic](#matching-custom-logic) 8 | 9 | 10 | ## Restrict access with scopes 11 | 12 | To restrict access based on the scopes a user has, use the `requiredScopes` middleware, raising a 403 `insufficient_scope` error if the value of the scope claim does not include all the given scopes. 13 | 14 | ```js 15 | const { 16 | auth, 17 | requiredScopes 18 | } = require('express-oauth2-jwt-bearer'); 19 | 20 | // Initialise the auth middleware with environment variables and restrict 21 | // access to your api to users with a valid Access Token JWT 22 | app.use(auth()); 23 | 24 | // Restrict access to the messages api to users with the `read:msg` 25 | // AND `write:msg` scopes 26 | app.get('/api/messages', 27 | requiredScopes('read:msg', 'write:msg'), 28 | (req, res, next) => { 29 | // ... 30 | } 31 | ); 32 | ``` 33 | 34 | ## Restrict access with claims 35 | 36 | ### Matching a specific value 37 | 38 | To restrict access based on the value of a claim use the `claimEquals` middleware. This checks that the claim exists and matches the expected value, raising a 401 `invalid_token` error if the value of the claim does not match. 39 | 40 | ```js 41 | const { 42 | auth, 43 | claimEquals 44 | } = require('express-oauth2-jwt-bearer'); 45 | 46 | // Initialise the auth middleware with environment variables and restrict 47 | // access to your api to users with a valid Access Token JWT 48 | app.use(auth()); 49 | 50 | // Restrict access to the admin api to users with the `isAdmin: true` claim 51 | app.get('/api/admin', claimEquals('isAdmin', true), (req, res, next) => { 52 | // ... 53 | }); 54 | ``` 55 | 56 | ### Matching multiple values 57 | 58 | To restrict access based on a claim including multiple values use the `claimIncludes` middleware. This checks that the claim exists and the expected values are included, rasising a 401 `invalid_token` error if the value of the claim does not include all the given values 59 | 60 | 61 | ```js 62 | const { 63 | auth, 64 | claimIncludes 65 | } = require('express-oauth2-jwt-bearer'); 66 | 67 | // Initialise the auth middleware with environment variables and restrict 68 | // access to your api to users with a valid Access Token JWT 69 | app.use(auth()); 70 | 71 | // Restrict access to the managers admin api to users with both the role `admin` 72 | // AND the role `manager` 73 | app.get('/api/admin/managers', 74 | claimIncludes('role', 'admin', 'manager'), 75 | (req, res, next) => { 76 | // ... 77 | } 78 | ); 79 | ``` 80 | 81 | ### Matching custom logic 82 | 83 | To restrict access based on custom logic you can provide a function use `claimCheck`. This must be a function that receives the JWT Payload and should return `true` if the token is valid, raising a 401 `invalid_token` error if the function returns `false`. 84 | 85 | ```js 86 | const { 87 | auth, 88 | claimCheck 89 | } = require('express-oauth2-jwt-bearer'); 90 | 91 | // Restrict access to the admin edit api to users with the `isAdmin: true` claim 92 | // and the `editor` role. 93 | app.get('/api/admin/edit', 94 | claimCheck(({ isAdmin, roles }) => isAdmin && roles.includes('editor')), 95 | (req, res, next) => { 96 | // ... 97 | } 98 | ); 99 | ``` -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Auth0 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 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/README.md: -------------------------------------------------------------------------------- 1 | ![Authentication middleware for Express.js that validates JWT Bearer Access Tokens](https://cdn.auth0.com/website/sdks/banners/express-oauth2-jwt-bearer-banner.png) 2 | 3 | [![npm](https://img.shields.io/npm/v/express-oauth2-jwt-bearer.svg?style=flat)](https://www.npmjs.com/package/express-oauth2-jwt-bearer) 4 | [![codecov](https://img.shields.io/badge/coverage-100%25-green)](./jest.config.js#L6-L13) 5 | ![Downloads](https://img.shields.io/npm/dw/express-oauth2-jwt-bearer) 6 | [![License](https://img.shields.io/:license-mit-blue.svg?style=flat)](https://opensource.org/licenses/MIT) 7 | [![CircleCI](https://img.shields.io/circleci/build/github/auth0/node-oauth2-jwt-bearer.svg?branch=master&style=flat)](https://circleci.com/gh/auth0/node-oauth2-jwt-bearer) 8 | 9 | 📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💻 [API Reference](#api-reference) - 💬 [Feedback](#feedback) 10 | 11 | ## Documentation 12 | 13 | - [Docs Site](https://auth0.com/docs) - explore our Docs site and learn more about Auth0. 14 | 15 | ## Getting started 16 | 17 | ### Requirements 18 | 19 | This package supports the following tooling versions: 20 | 21 | - Node.js: `^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0` 22 | 23 | ### Installation 24 | 25 | Using [npm](https://npmjs.org) in your project directory run the following command: 26 | 27 | ```shell 28 | npm install express-oauth2-jwt-bearer 29 | ``` 30 | 31 | ## Getting started 32 | 33 | ### Configure the SDK 34 | 35 | The library requires [issuerBaseURL](https://auth0.github.io/node-oauth2-jwt-bearer/interfaces/AuthOptions.html#issuerBaseURL) and [audience](https://auth0.github.io/node-oauth2-jwt-bearer/interfaces/AuthOptions.html#audience). 36 | 37 | #### Environment Variables 38 | 39 | ```shell 40 | ISSUER_BASE_URL=https://YOUR_ISSUER_DOMAIN 41 | AUDIENCE=https://my-api.com 42 | ``` 43 | 44 | ```js 45 | const { auth } = require('express-oauth2-jwt-bearer'); 46 | app.use(auth()); 47 | ``` 48 | 49 | #### Library Initialization 50 | 51 | ```js 52 | const { auth } = require('express-oauth2-jwt-bearer'); 53 | app.use( 54 | auth({ 55 | issuerBaseURL: 'https://YOUR_ISSUER_DOMAIN', 56 | audience: 'https://my-api.com', 57 | }) 58 | ); 59 | ``` 60 | 61 | #### JWTs signed with symmetric algorithms (eg `HS256`) 62 | 63 | ```js 64 | const { auth } = require('express-oauth2-jwt-bearer'); 65 | app.use( 66 | auth({ 67 | issuer: 'https://YOUR_ISSUER_DOMAIN', 68 | audience: 'https://my-api.com', 69 | secret: 'YOUR SECRET', 70 | tokenSigningAlg: 'HS256', 71 | }) 72 | ); 73 | ``` 74 | 75 | With this configuration, your api will require a valid Access Token JWT bearer token for all routes. 76 | 77 | Successful requests will have the following properties added to them: 78 | 79 | ```js 80 | app.get('/api/messages', (req, res, next) => { 81 | const auth = req.auth; 82 | auth.header; // The decoded JWT header. 83 | auth.payload; // The decoded JWT payload. 84 | auth.token; // The raw JWT token. 85 | }); 86 | ``` 87 | 88 | ### Security Headers 89 | 90 | Along with the other [security best practices](https://expressjs.com/en/advanced/best-practice-security.html) in the Express.js documentation, we recommend you use [helmet](https://www.npmjs.com/package/helmet) in addition to this middleware which can help protect your app from some well-known web vulnerabilities by setting default security HTTP headers. 91 | 92 | ### Error Handling 93 | 94 | This SDK raises errors with `err.status` and `err.headers` according to [rfc6750](https://datatracker.ietf.org/doc/html/rfc6750#section-3). The Express.js default error handler will set the error response with: 95 | 96 | - `res.statusCode` set from `err.status` 97 | - `res.statusMessage` set according to the status code. 98 | - The body will be the HTML of the status code message when in production environment, otherwise will be `err.stack`. 99 | - Any headers specified in an `err.headers` object. 100 | 101 | The `error_description` in the `WWW-Authenticate` header will contain useful information about the error, which you may not want to disclose in Production. 102 | 103 | See the Express.js [docs on error handling](https://expressjs.com/en/guide/error-handling.html) for more information on writing custom error handlers. 104 | 105 | ## API Reference 106 | 107 | - [auth](https://auth0.github.io/node-oauth2-jwt-bearer/functions/auth.html) - Middleware that will return a 401 if a valid Access token JWT bearer token is not provided in the request. 108 | - [AuthResult](https://auth0.github.io/node-oauth2-jwt-bearer/interfaces/AuthResult.html) - The properties added to `req.auth` upon successful authorization. 109 | - [requiredScopes](https://auth0.github.io/node-oauth2-jwt-bearer/functions/requiredScopes.html) - Check a token's scope claim to include a number of given scopes, raises a 403 `insufficient_scope` error if the value of the scope claim does not include all the given scopes. 110 | - [claimEquals](https://auth0.github.io/node-oauth2-jwt-bearer/functions/claimEquals.html) - Check a token's claim to be equal a given JSONPrimitive (string, number, boolean or null) raises a 401 `invalid_token` error if the value of the claim does not match. 111 | - [claimIncludes](https://auth0.github.io/node-oauth2-jwt-bearer/functions/claimIncludes.html) - Check a token's claim to include a number of given JSONPrimitives (string, number, boolean or null) raises a 401 `invalid_token` error if the value of the claim does not include all the given values. 112 | - [claimCheck](https://auth0.github.io/node-oauth2-jwt-bearer/functions/claimCheck.html) - Check the token's claims using a custom method that receives the JWT Payload and should return `true` if the token is valid. Raises a 401 `invalid_token` error if the function returns `false`. 113 | 114 | ## Feedback 115 | 116 | ### Contributing 117 | 118 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: 119 | 120 | - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 121 | - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) 122 | - [This repo's contribution guide](https://github.com/auth0/node-oauth2-jwt-bearer/blob/main/CONTRIBUTING.md) 123 | 124 | ### Raise an issue 125 | 126 | To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/node-oauth2-jwt-bearer/issues). 127 | 128 | ### Vulnerability Reporting 129 | 130 | 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. 131 | 132 | ## What is Auth0? 133 | 134 |

135 | 136 | 137 | 138 | Auth0 Logo 139 | 140 |

141 |

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

144 |

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

147 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest/presets/js-with-ts', 3 | testEnvironment: 'node', 4 | collectCoverageFrom: ['src/*'], 5 | coverageThreshold: { 6 | global: { 7 | branches: 100, 8 | functions: 100, 9 | lines: 100, 10 | statements: 100, 11 | }, 12 | }, 13 | reporters: [ 14 | 'default', 15 | [ 16 | 'jest-junit', 17 | { 18 | suiteName: 'express-oauth2-jwt-bearer', 19 | outputDirectory: '../../test-results/express-oauth2-jwt-bearer', 20 | }, 21 | ], 22 | ], 23 | moduleNameMapper: { 24 | '^oauth2-bearer$': '/../oauth2-bearer/src/', 25 | '^access-token-jwt$': '/../access-token-jwt/src/', 26 | }, 27 | transform: { 28 | '^.+\\.tsx?$': [ 29 | 'ts-jest', 30 | { 31 | tsconfig: { 32 | baseUrl: '.', 33 | paths: { 34 | 'oauth2-bearer': ['../oauth2-bearer/src'], 35 | 'access-token-jwt': ['../access-token-jwt/src'], 36 | }, 37 | useUnknownInCatchVariables: false, 38 | }, 39 | }, 40 | ], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-oauth2-jwt-bearer", 3 | "description": "Authentication middleware for Express.js that validates JWT bearer access tokens.", 4 | "version": "1.6.1", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "auth0/node-oauth2-jwt-bearer", 8 | "scripts": { 9 | "test": "jest test --coverage", 10 | "lint": "eslint --fix --ext .ts ./src", 11 | "prebuild": "rimraf dist", 12 | "build": "rollup -c" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Auth0 ", 18 | "homepage": "https://github.com/auth0/node-oauth2-jwt-bearer/tree/main/packages/express-oauth2-jwt-bearer", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@rollup/plugin-node-resolve": "^13.3.0", 22 | "@tsconfig/node12": "^1.0.11", 23 | "@types/express": "^4.17.17", 24 | "@types/jest": "^27.5.2", 25 | "@types/node": "^14.18.42", 26 | "@typescript-eslint/eslint-plugin": "^5.57.0", 27 | "@typescript-eslint/parser": "^5.57.0", 28 | "eslint": "^8.37.0", 29 | "express": "^4.18.2", 30 | "got": "^11.8.6", 31 | "jest": "^29.5.0", 32 | "jest-junit": "^13.2.0", 33 | "nock": "^13.3.0", 34 | "prettier": "~2.5.1", 35 | "rimraf": "^3.0.2", 36 | "rollup": "^2.79.1", 37 | "rollup-plugin-dts": "^4.2.3", 38 | "rollup-plugin-typescript2": "^0.31.2", 39 | "ts-jest": "^29.0.5", 40 | "tslib": "^2.5.0", 41 | "typescript": "^5.0.2" 42 | }, 43 | "dependencies": { 44 | "jose": "^4.15.5" 45 | }, 46 | "engines": { 47 | "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import dts from 'rollup-plugin-dts'; 4 | 5 | export default [ 6 | { 7 | input: 'src/index.ts', 8 | output: { 9 | dir: 'dist', 10 | format: 'cjs', 11 | }, 12 | external: ['jose'], 13 | plugins: [ 14 | nodeResolve(), 15 | typescript({ 16 | tsconfigOverride: { compilerOptions: { module: 'ES2015' } }, 17 | }), 18 | ], 19 | }, 20 | { 21 | input: 'src/index.ts', 22 | output: { 23 | dir: 'dist', 24 | format: 'es', 25 | }, 26 | external: ['express', 'http', 'https'], 27 | plugins: [dts({ respectExternal: true })], 28 | }, 29 | ]; 30 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler, NextFunction, Request, Response } from 'express'; 2 | import { 3 | jwtVerifier, 4 | JwtVerifierOptions, 5 | claimCheck as _claimCheck, 6 | ClaimCheck, 7 | claimEquals as _claimEquals, 8 | ClaimEquals, 9 | claimIncludes as _claimIncludes, 10 | ClaimIncludes, 11 | requiredScopes as _requiredScopes, 12 | RequiredScopes, 13 | scopeIncludesAny as _scopeIncludesAny, 14 | VerifyJwtResult as AuthResult, 15 | } from 'access-token-jwt'; 16 | import type { JWTPayload } from 'access-token-jwt'; 17 | import { getToken } from 'oauth2-bearer'; 18 | 19 | export interface AuthOptions extends JwtVerifierOptions { 20 | /** 21 | * True if a valid Access Token JWT should be required for all routes. 22 | * Defaults to true. 23 | */ 24 | authRequired?: boolean; 25 | } 26 | 27 | declare global { 28 | namespace Express { 29 | interface Request { 30 | auth?: AuthResult; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Middleware that will return a 401 if a valid JWT bearer token is not provided 37 | * in the request. 38 | * 39 | * Can be used in 2 ways: 40 | * 41 | * 1. Pass in an {@Link AuthOptions.issuerBaseURL} (or define the env 42 | * variable `ISSUER_BASE_URL`) 43 | * 44 | * ```js 45 | * app.use(auth({ 46 | * issuerBaseURL: 'http://issuer.example.com', 47 | * audience: 'https://myapi.com' 48 | * })); 49 | * ``` 50 | * 51 | * This uses the {@Link AuthOptions.issuerBaseURL} to find the OAuth 2.0 52 | * Authorization Server Metadata to get the {@Link AuthOptions.jwksUri} 53 | * and {@Link AuthOptions.issuer}. 54 | * 55 | * 2. You can also skip discovery and provide the {@Link AuthOptions.jwksUri} (or 56 | * define the env variable `JWKS_URI`) and {@Link AuthOptions.issuer} (or define 57 | * the env variable `ISSUER`) yourself. 58 | * 59 | * ```js 60 | * app.use(auth({ 61 | * jwksUri: 'http://issuer.example.com/well-known/jwks.json', 62 | * issuer: 'http://issuer.example.com', 63 | * audience: 'https://myapi.com' 64 | * })); 65 | * ``` 66 | * 67 | * You must provide the `audience` argument (or `AUDIENCE` environment variable) 68 | * used to match against the Access Token's `aud` claim. 69 | * 70 | * Successful requests will have the following properties added to them: 71 | * 72 | * ```js 73 | * app.get('/foo', auth(), (req, res, next) => { 74 | * const auth = req.auth; 75 | * auth.header; // The decoded JWT header. 76 | * auth.payload; // The decoded JWT payload. 77 | * auth.token; // The raw JWT token. 78 | * }); 79 | * ``` 80 | * 81 | */ 82 | export const auth = (opts: AuthOptions = {}): Handler => { 83 | const verifyJwt = jwtVerifier(opts); 84 | 85 | return async (req: Request, res: Response, next: NextFunction) => { 86 | try { 87 | const jwt = getToken( 88 | req.headers, 89 | req.query, 90 | req.body, 91 | !!req.is('urlencoded') 92 | ); 93 | req.auth = await verifyJwt(jwt); 94 | next(); 95 | } catch (e) { 96 | if (opts.authRequired === false) { 97 | next(); 98 | } else { 99 | next(e); 100 | } 101 | } 102 | }; 103 | }; 104 | 105 | const toHandler = 106 | (fn: (payload?: JWTPayload) => void): Handler => 107 | (req, res, next) => { 108 | try { 109 | fn(req.auth?.payload); 110 | next(); 111 | } catch (e) { 112 | next(e); 113 | } 114 | }; 115 | 116 | /** 117 | * Check the token's claims using a custom method that receives the 118 | * {@Link JWTPayload} and should return `true` if the token is valid. Raises 119 | * a 401 `invalid_token` error if the function returns false. You can also 120 | * customise the `error_description` which should be formatted per rfc6750. 121 | * 122 | * ```js 123 | * app.use(auth()); 124 | * 125 | * app.get('/admin/edit', claimCheck((claims) => { 126 | * return claims.isAdmin && claims.roles.includes('editor'); 127 | * }, `Unexpected 'isAdmin' and 'roles' claims`), (req, res) => { ... }); 128 | * ``` 129 | */ 130 | export const claimCheck: ClaimCheck = (...args) => 131 | toHandler(_claimCheck(...args)); 132 | 133 | /** 134 | * Check a token's claim to be equal a given {@Link JSONPrimitive} 135 | * (`string`, `number`, `boolean` or `null`) raises a 401 `invalid_token` 136 | * error if the value of the claim does not match. 137 | * 138 | * ```js 139 | * app.use(auth()); 140 | * 141 | * app.get('/admin', claimEquals('isAdmin', true), (req, res) => { ... }); 142 | * ``` 143 | */ 144 | export const claimEquals: ClaimEquals = (...args) => 145 | toHandler(_claimEquals(...args)); 146 | 147 | /** 148 | * Check a token's claim to include a number of given {@Link JSONPrimitive}s 149 | * (`string`, `number`, `boolean` or `null`) raises a 401 `invalid_token` 150 | * error if the value of the claim does not include all the given values. 151 | * 152 | * ```js 153 | * app.use(auth()); 154 | * 155 | * app.get('/admin/edit', claimIncludes('role', 'admin', 'editor'), 156 | * (req, res) => { ... }); 157 | * ``` 158 | */ 159 | export const claimIncludes: ClaimIncludes = (...args) => 160 | toHandler(_claimIncludes(...args)); 161 | 162 | /** 163 | * Check a token's `scope` claim to include a number of given scopes, raises a 164 | * 403 `insufficient_scope` error if the value of the `scope` claim does not 165 | * include all the given scopes. 166 | * 167 | * ```js 168 | * app.use(auth()); 169 | * 170 | * app.get('/admin/edit', requiredScopes('read:admin write:admin'), 171 | * (req, res) => { ... }); 172 | * ``` 173 | */ 174 | export const requiredScopes: RequiredScopes = (...args) => 175 | toHandler(_requiredScopes(...args)); 176 | 177 | /** 178 | * Check a token's `scope` claim to include any of the given scopes, raises a 179 | * 403 `insufficient_scope` error if the value of the `scope` claim does not 180 | * include any of the given scopes. 181 | * 182 | * ```js 183 | * app.use(auth()); 184 | * 185 | * app.get('/admin/edit', scopeIncludesAny('read:msg read:admin'), 186 | * (req, res) => { ... }); 187 | * ``` 188 | */ 189 | export const scopeIncludesAny: RequiredScopes = (...args) => 190 | toHandler(_scopeIncludesAny(...args)); 191 | 192 | export { AuthResult, JWTPayload }; 193 | export { 194 | FunctionValidator, 195 | Validator, 196 | Validators, 197 | JWTHeader, 198 | JSONPrimitive, 199 | } from 'access-token-jwt'; 200 | export { 201 | UnauthorizedError, 202 | InvalidRequestError, 203 | InvalidTokenError, 204 | InsufficientScopeError, 205 | } from 'oauth2-bearer'; 206 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "removeComments": true, 6 | "outDir": "./dist" 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /packages/express-oauth2-jwt-bearer/typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | out: './docs/', 3 | excludePrivate: false, 4 | excludeExternals: false, 5 | hideGenerator: true, 6 | readme: 'none', 7 | gitRevision: process.env.npm_package_version, 8 | entryPoints: ['src', '../access-token-jwt/src'], 9 | toc: false, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "ignorePatterns": ["*.js"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/README.md: -------------------------------------------------------------------------------- 1 | # oauth2-bearer 2 | 3 | _This package is not published_ 4 | 5 | Gets Bearer tokens from a request and issues errors per [rfc6750](https://tools.ietf.org/html/rfc6750) 6 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | reporters: [ 4 | 'default', 5 | [ 6 | 'jest-junit', 7 | { 8 | suiteName: 'oauth2-bearer', 9 | outputDirectory: '../../test-results/oauth2-bearer', 10 | }, 11 | ], 12 | ], 13 | coverageThreshold: { 14 | global: { 15 | branches: 100, 16 | functions: 100, 17 | lines: 100, 18 | statements: 100, 19 | }, 20 | }, 21 | transform: { 22 | '^.+\\.tsx?$': [ 23 | 'ts-jest', 24 | { 25 | tsconfig: { 26 | module: 'commonjs', 27 | }, 28 | }, 29 | ], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth2-bearer", 3 | "private": true, 4 | "license": "MIT", 5 | "author": "Auth0 ", 6 | "version": "0.0.1", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "test": "jest test --coverage", 11 | "lint": "eslint --fix --ext .ts ./src", 12 | "build": "tsc", 13 | "prebuild": "rimraf dist" 14 | }, 15 | "devDependencies": { 16 | "@tsconfig/node12": "^1.0.11", 17 | "@types/body": "^5.1.1", 18 | "@types/jest": "^27.5.2", 19 | "@types/node": "^14.18.42", 20 | "@types/type-is": "^1.6.3", 21 | "@typescript-eslint/eslint-plugin": "^5.57.0", 22 | "@typescript-eslint/parser": "^5.57.0", 23 | "body": "^5.1.0", 24 | "eslint": "^8.37.0", 25 | "got": "^11.8.6", 26 | "jest": "^29.5.0", 27 | "jest-junit": "^13.2.0", 28 | "prettier": "~2.5.1", 29 | "rimraf": "^3.0.2", 30 | "ts-jest": "^29.0.5", 31 | "type-is": "^1.6.18", 32 | "typescript": "^5.0.2" 33 | }, 34 | "engines": { 35 | "node": ">=12.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/src/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Errors per https://tools.ietf.org/html/rfc6750#section-3.1 3 | */ 4 | 5 | /** 6 | * If the request lacks any authentication information, 7 | * the resource server SHOULD NOT include an error code or 8 | * other error information. 9 | */ 10 | export class UnauthorizedError extends Error { 11 | status = 401; 12 | statusCode = 401; 13 | headers = { 'WWW-Authenticate': 'Bearer realm="api"' }; 14 | 15 | constructor(message = 'Unauthorized') { 16 | super(message); 17 | this.name = this.constructor.name; 18 | } 19 | } 20 | 21 | /** 22 | * The request is missing a required parameter, includes an 23 | * unsupported parameter or parameter value, repeats the same 24 | * parameter, uses more than one method for including an access 25 | * token, or is otherwise malformed. 26 | */ 27 | export class InvalidRequestError extends UnauthorizedError { 28 | code = 'invalid_request'; 29 | status = 400; 30 | statusCode = 400; 31 | 32 | constructor(message = 'Invalid Request') { 33 | super(message); 34 | this.headers = getHeaders(this.code, this.message); 35 | } 36 | } 37 | 38 | /** 39 | * The access token provided is expired, revoked, malformed, or 40 | * invalid for other reasons. 41 | */ 42 | export class InvalidTokenError extends UnauthorizedError { 43 | code = 'invalid_token'; 44 | status = 401; 45 | statusCode = 401; 46 | 47 | constructor(message = 'Invalid Token') { 48 | super(message); 49 | this.headers = getHeaders(this.code, this.message); 50 | } 51 | } 52 | 53 | /** 54 | * The request requires higher privileges than provided by the 55 | * access token. 56 | */ 57 | export class InsufficientScopeError extends UnauthorizedError { 58 | code = 'insufficient_scope'; 59 | status = 403; 60 | statusCode = 403; 61 | 62 | constructor(scopes?: string[], message = 'Insufficient Scope') { 63 | super(message); 64 | this.headers = getHeaders(this.code, this.message, scopes); 65 | } 66 | } 67 | 68 | // Generate a response header per https://tools.ietf.org/html/rfc6750#section-3 69 | const getHeaders = (error: string, description: string, scopes?: string[]) => ({ 70 | 'WWW-Authenticate': `Bearer realm="api", error="${error}", error_description="${description.replace( 71 | /"/g, 72 | "'" 73 | )}"${(scopes && `, scope="${scopes.join(' ')}"`) || ''}`, 74 | }); 75 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/src/get-token.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a Bearer Token from a request per https://tools.ietf.org/html/rfc6750#section-2 3 | */ 4 | import { InvalidRequestError, UnauthorizedError } from './errors'; 5 | 6 | type QueryLike = Record & { access_token?: string }; 7 | type BodyLike = QueryLike; 8 | type HeadersLike = Record & { 9 | authorization?: string; 10 | 'content-type'?: string; 11 | }; 12 | 13 | const TOKEN_RE = /^Bearer (.+)$/i; 14 | 15 | const getTokenFromHeader = (headers: HeadersLike) => { 16 | if (typeof headers.authorization !== 'string') { 17 | return; 18 | } 19 | const match = headers.authorization.match(TOKEN_RE); 20 | if (!match) { 21 | return; 22 | } 23 | return match[1]; 24 | }; 25 | 26 | const getTokenFromQuery = (query?: QueryLike) => { 27 | const accessToken = query?.access_token; 28 | if (typeof accessToken === 'string') { 29 | return accessToken; 30 | } 31 | }; 32 | 33 | const getFromBody = (body?: BodyLike, urlEncoded?: boolean) => { 34 | const accessToken = body?.access_token; 35 | if (typeof accessToken === 'string' && urlEncoded) { 36 | return accessToken; 37 | } 38 | }; 39 | 40 | /** 41 | * Get a Bearer Token from a request. 42 | * 43 | * @param headers An object containing the request headers, usually `req.headers`. 44 | * @param query An object containing the request query parameters, usually `req.query`. 45 | * @param body An object containing the request payload, usually `req.body` or `req.payload`. 46 | * @param urlEncoded true if the request's Content-Type is `application/x-www-form-urlencoded`. 47 | */ 48 | export default function getToken( 49 | headers: HeadersLike, 50 | query?: QueryLike, 51 | body?: BodyLike, 52 | urlEncoded?: boolean 53 | ): string { 54 | const fromHeader = getTokenFromHeader(headers); 55 | const fromQuery = getTokenFromQuery(query); 56 | const fromBody = getFromBody(body, urlEncoded); 57 | 58 | if (!fromQuery && !fromHeader && !fromBody) { 59 | throw new UnauthorizedError(); 60 | } 61 | 62 | if (+!!fromQuery + +!!fromBody + +!!fromHeader > 1) { 63 | throw new InvalidRequestError( 64 | 'More than one method used for authentication' 65 | ); 66 | } 67 | 68 | return (fromQuery || fromBody || fromHeader) as string; 69 | } 70 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export { default as getToken } from './get-token'; 3 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/test/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UnauthorizedError, 3 | InvalidRequestError, 4 | InvalidTokenError, 5 | InsufficientScopeError, 6 | } from '../src'; 7 | 8 | describe('errors', () => { 9 | it('should raise an Unauthorized error', () => { 10 | expect(new UnauthorizedError()).toMatchObject({ 11 | headers: { 12 | 'WWW-Authenticate': 'Bearer realm="api"', 13 | }, 14 | message: 'Unauthorized', 15 | name: 'UnauthorizedError', 16 | status: 401, 17 | statusCode: 401, 18 | }); 19 | }); 20 | 21 | it('should raise an Invalid Request error', () => { 22 | expect(new InvalidRequestError()).toMatchObject({ 23 | code: 'invalid_request', 24 | headers: { 25 | 'WWW-Authenticate': 26 | 'Bearer realm="api", error="invalid_request", error_description="Invalid Request"', 27 | }, 28 | message: 'Invalid Request', 29 | name: 'InvalidRequestError', 30 | status: 400, 31 | statusCode: 400, 32 | }); 33 | }); 34 | 35 | it('should raise an Invalid Request error with a custom message', () => { 36 | expect(new InvalidRequestError('Custom Message')).toMatchObject({ 37 | code: 'invalid_request', 38 | headers: { 39 | 'WWW-Authenticate': 40 | 'Bearer realm="api", error="invalid_request", error_description="Custom Message"', 41 | }, 42 | message: 'Custom Message', 43 | name: 'InvalidRequestError', 44 | status: 400, 45 | statusCode: 400, 46 | }); 47 | }); 48 | 49 | it('should avoid nested double quotes in header', () => { 50 | expect(new InvalidRequestError('expected "foo" got "bar"')).toMatchObject({ 51 | code: 'invalid_request', 52 | headers: { 53 | 'WWW-Authenticate': `Bearer realm="api", error="invalid_request", error_description="expected 'foo' got 'bar'"`, 54 | }, 55 | message: 'expected "foo" got "bar"', 56 | name: 'InvalidRequestError', 57 | status: 400, 58 | statusCode: 400, 59 | }); 60 | }); 61 | 62 | it('should raise an Invalid Token error', () => { 63 | expect(new InvalidTokenError()).toMatchObject({ 64 | code: 'invalid_token', 65 | headers: { 66 | 'WWW-Authenticate': 67 | 'Bearer realm="api", error="invalid_token", error_description="Invalid Token"', 68 | }, 69 | message: 'Invalid Token', 70 | name: 'InvalidTokenError', 71 | status: 401, 72 | statusCode: 401, 73 | }); 74 | }); 75 | 76 | it('should raise an Insufficient Scope error', () => { 77 | expect(new InsufficientScopeError(['foo', 'bar'])).toMatchObject({ 78 | code: 'insufficient_scope', 79 | headers: { 80 | 'WWW-Authenticate': 81 | 'Bearer realm="api", error="insufficient_scope", error_description="Insufficient Scope", scope="foo bar"', 82 | }, 83 | message: 'Insufficient Scope', 84 | name: 'InsufficientScopeError', 85 | status: 403, 86 | statusCode: 403, 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/test/get-token.test.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; 2 | import { URL } from 'url'; 3 | import { AddressInfo } from 'net'; 4 | import anyBody = require('body/any'); 5 | import got from 'got'; 6 | import typeis = require('type-is'); 7 | import { getToken } from '../src'; 8 | 9 | const start = (server: Server): Promise => 10 | new Promise((resolve) => 11 | server.listen(0, () => 12 | resolve(`http://localhost:${(server.address() as AddressInfo).port}`) 13 | ) 14 | ); 15 | 16 | const handler = (req: IncomingMessage, res: ServerResponse) => { 17 | let query: Record; 18 | new URL(req.url as string, 'http://localhost').searchParams.forEach( 19 | (val, key) => { 20 | query = query || {}; 21 | query[key] = val; 22 | } 23 | ); 24 | anyBody(req, res, (err, body) => { 25 | try { 26 | res.end( 27 | getToken( 28 | req.headers, 29 | query, 30 | body as Record, 31 | !!typeis.is(req.headers['content-type'] as string, ['urlencoded']) 32 | ) 33 | ); 34 | } catch (e) { 35 | res.statusCode = e.statusCode; 36 | res.statusMessage = e.message; 37 | res.end(); 38 | } 39 | }); 40 | }; 41 | 42 | describe('get-token', () => { 43 | let server: Server; 44 | let url: string; 45 | 46 | beforeEach(async () => { 47 | server = createServer(handler); 48 | url = await start(server); 49 | }); 50 | 51 | afterEach((done) => { 52 | server.close(done); 53 | }); 54 | 55 | it('should fail when there are no tokens', async () => { 56 | await expect(got(url)).rejects.toThrowError( 57 | 'Response code 401 (Unauthorized)' 58 | ); 59 | }); 60 | 61 | it('should get the token from the header', async () => { 62 | await expect( 63 | got(url, { 64 | resolveBodyOnly: true, 65 | headers: { 66 | authorization: 'Bearer token', 67 | }, 68 | }) 69 | ).resolves.toEqual('token'); 70 | }); 71 | 72 | it('should do case insensitive check for header', async () => { 73 | await expect( 74 | got(url, { 75 | resolveBodyOnly: true, 76 | headers: { 77 | authorization: 'bearer token', 78 | }, 79 | }) 80 | ).resolves.toEqual('token'); 81 | }); 82 | 83 | it('should fail for malformed header', async () => { 84 | await expect( 85 | got(url, { 86 | headers: { 87 | authorization: 'foo token', 88 | }, 89 | }) 90 | ).rejects.toThrowError('Response code 401 (Unauthorized)'); 91 | }); 92 | 93 | it('should fail for empty header', async () => { 94 | await expect( 95 | got(url, { 96 | headers: { 97 | authorization: 'Bearer ', 98 | }, 99 | }) 100 | ).rejects.toThrowError('Response code 401 (Unauthorized)'); 101 | }); 102 | 103 | it('should get the token from the query string', async () => { 104 | await expect( 105 | got(url, { 106 | resolveBodyOnly: true, 107 | searchParams: { access_token: 'token' }, 108 | }) 109 | ).resolves.toEqual('token'); 110 | }); 111 | 112 | it('should succeed to get the token from the query string for POST requests', async () => { 113 | await expect( 114 | got(url, { 115 | resolveBodyOnly: true, 116 | method: 'POST', 117 | searchParams: { access_token: 'token' }, 118 | }) 119 | ).resolves.toEqual('token'); 120 | }); 121 | 122 | it('should get the token from the request payload', async () => { 123 | await expect( 124 | got(url, { 125 | resolveBodyOnly: true, 126 | method: 'POST', 127 | form: { access_token: 'token' }, 128 | }) 129 | ).resolves.toEqual('token'); 130 | }); 131 | 132 | it('should fail to get the token from JSON request payload', async () => { 133 | await expect( 134 | got(url, { 135 | method: 'POST', 136 | json: { access_token: 'token' }, 137 | }) 138 | ).rejects.toThrowError('Response code 401 (Unauthorized)'); 139 | }); 140 | 141 | it('should succeed to get the token from request payload for GETs', async () => { 142 | await expect( 143 | got(url, { 144 | resolveBodyOnly: true, 145 | allowGetBody: true, 146 | method: 'GET', 147 | form: { access_token: 'token' }, 148 | }) 149 | ).resolves.toEqual('token'); 150 | }); 151 | 152 | it('should fail if more than one method is used', async () => { 153 | await expect( 154 | got(url, { 155 | searchParams: { access_token: 'token' }, 156 | headers: { 157 | authorization: 'Bearer token', 158 | }, 159 | }) 160 | ).rejects.toThrowError( 161 | 'Response code 400 (More than one method used for authentication)' 162 | ); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /packages/oauth2-bearer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "useUnknownInCatchVariables": false, 6 | "module": "ES2020", 7 | "removeComments": true, 8 | "declaration": true, 9 | "outDir": "./dist" 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node12/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "access-token-jwt": ["packages/access-token-jwt/src"], 7 | "oauth2-bearer": ["packages/oauth2-bearer/src"] 8 | }, 9 | "useUnknownInCatchVariables": false, 10 | }, 11 | "include": [ 12 | "packages/oauth2-bearer/src", 13 | "packages/access-token-jwt/src", 14 | "packages/express-oauth2-jwt-bearer/src", 15 | ] 16 | } 17 | 18 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | out: "./docs/", 3 | excludePrivate: false, 4 | excludeExternals: false, 5 | hideGenerator: true, 6 | readme: "none", 7 | entryPoints: ["packages/express-oauth2-jwt-bearer/src"], 8 | tsconfig: "tsconfig.typedoc.json", 9 | }; 10 | --------------------------------------------------------------------------------