├── .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 |
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
11 |
12 |
13 |
14 |
17 |
Function requiredScopes
18 |
19 |
20 | required Scopes ( scopes ) : Handler
21 |
22 |
28 |
29 |
Parameters
30 |
31 |
32 | scopes : string | string []
33 | Returns Handler
36 |
69 |
--------------------------------------------------------------------------------
/docs/functions/scopeIncludesAny.html:
--------------------------------------------------------------------------------
1 | scopeIncludesAny | express-oauth2-jwt-bearer
11 |
12 |
13 |
14 |
17 |
Function scopeIncludesAny
18 |
19 |
20 | scope Includes Any ( scopes ) : Handler
21 |
22 |
28 |
29 |
Parameters
30 |
31 |
32 | scopes : string | string []
33 | Returns Handler
36 |
69 |
--------------------------------------------------------------------------------
/docs/types/JSONPrimitive.html:
--------------------------------------------------------------------------------
1 | JSONPrimitive | express-oauth2-jwt-bearer
11 |
12 |
13 |
14 |
17 |
Type alias JSONPrimitive
18 |
JSONPrimitive : string | number | boolean | null
21 |
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 | auth()
51 | requiredScopes('read:msg')
52 | scopeIncludesAny(['read:msg', 'audit:read'])
53 | claimEquals('foo', 'bar')
54 | claimIncludes('foo', 'bar', 'baz')
55 | auth({ validators: { iss: false } })
56 | auth({ strict: true })
57 | auth({ secret: '...' })
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 | 
2 |
3 | [](https://www.npmjs.com/package/express-oauth2-jwt-bearer)
4 | [](./jest.config.js#L6-L13)
5 | 
6 | [](https://opensource.org/licenses/MIT)
7 | [](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 |
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 |
--------------------------------------------------------------------------------
Check a token's
25 | 27 |scope
claim to include a number of given scopes, raises a 23 | 403insufficient_scope
error if the value of thescope
claim does not 24 | include all the given scopes.