├── .version ├── .husky └── pre-commit ├── .semgrepignore ├── .prettierrc ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── Feature Request.yml │ └── Bug Report.yml ├── dependabot.yml ├── actions │ ├── get-version │ │ └── action.yml │ ├── get-prerelease │ │ └── action.yml │ ├── tag-exists │ │ └── action.yml │ ├── release-create │ │ └── action.yml │ ├── npm-publish │ │ └── action.yml │ └── get-release-notes │ │ └── action.yml ├── workflows │ ├── semgrep.yml │ ├── release.yml │ ├── snyk.yml │ ├── build.yml │ └── npm-release.yml └── stale.yml ├── .shiprc ├── opslevel.yml ├── tsconfig.esm.json ├── bs-config.json ├── tsconfig.cjs.json ├── tsconfig.json ├── tsconfig.build.json ├── .gitignore ├── jest.config.ts ├── .eslintrc.json ├── LICENSE ├── static └── index.html ├── lib ├── index.ts └── index.test.ts ├── package.json ├── README.md └── CHANGELOG.md /.version: -------------------------------------------------------------------------------- 1 | v4.0.0 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.semgrepignore: -------------------------------------------------------------------------------- 1 | lib/index.test.ts 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0/project-dx-sdks-engineer-codeowner 2 | -------------------------------------------------------------------------------- /.shiprc: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | ".version": [] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_sdks 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./build/esm" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /bs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 3000, 3 | "watch": true, 4 | "server": ["build", "static"], 5 | "files": ["build", "static"] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "outDir": "./build/cjs", 5 | "module": "CommonJS" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "noEmitOnError": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./lib"], 4 | "exclude": ["**/*.test.ts"], 5 | "compilerOptions": { 6 | "target": "ES2017", 7 | "declaration": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Help & Questions 4 | url: https://community.auth0.com 5 | about: Ask general support or usage questions in the Auth0 Community forums 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by http://gitignore.io 2 | 3 | ### Node ### 4 | lib-cov 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.gz 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | build/* 20 | coverage/* -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { type JestConfigWithTsJest } from "ts-jest"; 2 | 3 | const jestConfig: JestConfigWithTsJest = { 4 | preset: "ts-jest/presets/default-esm", 5 | moduleNameMapper: { 6 | "^(\\.{1,2}/.*)\\.js$": "$1", 7 | }, 8 | coverageProvider: "v8", 9 | coverageReporters: ["lcov", "text", "text-summary"], 10 | }; 11 | 12 | export default jestConfig; 13 | -------------------------------------------------------------------------------- /.github/actions/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the version extracted from the branch name 2 | 3 | # 4 | # Returns the version from the .version file. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | outputs: 10 | version: 11 | value: ${{ steps.get_version.outputs.VERSION }} 12 | 13 | runs: 14 | using: composite 15 | 16 | steps: 17 | - id: get_version 18 | shell: bash 19 | run: | 20 | VERSION=$(head -1 .version) 21 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 22 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | pull_request: {} 5 | 6 | push: 7 | branches: ["master", "main"] 8 | 9 | schedule: 10 | - cron: '30 0 1,15 * *' 11 | 12 | jobs: 13 | semgrep: 14 | name: Scan 15 | runs-on: ubuntu-latest 16 | container: 17 | image: returntocorp/semgrep 18 | if: (github.actor != 'dependabot[bot]') 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - run: semgrep ci 23 | env: 24 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": ["build", "coverage"], 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended-type-checked", 6 | "plugin:@typescript-eslint/stylistic-type-checked", 7 | "plugin:import/recommended", 8 | "plugin:import/typescript", 9 | "plugin:prettier/recommended" 10 | ], 11 | "overrides": [ 12 | { 13 | "files": ["lib/**"], 14 | "rules": { 15 | "import/no-default-export": "error" 16 | } 17 | } 18 | ], 19 | "settings": { 20 | "import/resolver": { 21 | "typescript": true 22 | } 23 | }, 24 | "parserOptions": { 25 | "project": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.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/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create npm and GitHub Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | id-token: write # For publishing to npm using --provenance 12 | 13 | ### TODO: Replace instances of './.github/workflows/' w/ `auth0/dx-sdk-actions/workflows/` and append `@latest` after the common `dx-sdk-actions` repo is made public. 14 | ### TODO: Also remove `get-prerelease`, `get-release-notes`, `get-version`, `npm-publish`, `release-create`, and `tag-exists` actions from this repo's .github/actions folder once the repo is public. 15 | ### TODO: Also remove `npm-release` workflow from this repo's .github/workflows folder once the repo is public. 16 | 17 | jobs: 18 | release: 19 | uses: ./.github/workflows/npm-release.yml 20 | with: 21 | node-version: 18 22 | require-build: true 23 | secrets: 24 | npm-token: ${{ secrets.NPM_TOKEN }} 25 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | daysUntilClose: 7 8 | 9 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 10 | exemptLabels: [] 11 | 12 | # Set to true to ignore issues with an assignee (defaults to false) 13 | exemptAssignees: true 14 | 15 | # Label to use when marking as stale 16 | staleLabel: closed:stale 17 | 18 | # Comment to post when marking as stale. Set to `false` to disable 19 | markComment: > 20 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ -------------------------------------------------------------------------------- /.github/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/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 | check: 25 | name: Check for Vulnerabilities 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 30 | 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. 31 | 32 | - uses: actions/checkout@v4 33 | with: 34 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 35 | 36 | - uses: snyk/actions/node@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0 37 | env: 38 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Auth0, Inc. (http://auth0.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.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/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main, beta ] 7 | pull_request: 8 | branches: [ main, beta ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 16 | 17 | jobs: 18 | 19 | build: 20 | strategy: 21 | matrix: 22 | node: [ 18, 20 ] 23 | 24 | name: Build Package (Node ${{ matrix.node }}) 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v4 30 | - name: Setup Node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node }} 34 | cache: npm 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | 39 | - name: Build package 40 | run: npm run build 41 | 42 | - name: Lint 43 | run: npm run lint && npm run lint:package 44 | 45 | - name: Run tests against browser 46 | run: npm run test:browser 47 | 48 | - name: Run tests against Node 49 | run: npm run test:node 50 | -------------------------------------------------------------------------------- /.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 }} 53 | -------------------------------------------------------------------------------- /.github/actions/get-release-notes/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the release notes extracted from the body of the PR associated with the release. 2 | 3 | # 4 | # Returns the release notes from the content of a pull request linked to a release branch. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | inputs: 9 | version: 10 | required: true 11 | repo_name: 12 | required: false 13 | repo_owner: 14 | required: true 15 | token: 16 | required: true 17 | 18 | outputs: 19 | release-notes: 20 | value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} 21 | 22 | runs: 23 | using: composite 24 | 25 | steps: 26 | - uses: actions/github-script@v7 27 | id: get_release_notes 28 | with: 29 | result-encoding: string 30 | script: | 31 | const { data: pulls } = await github.rest.pulls.list({ 32 | owner: process.env.REPO_OWNER, 33 | repo: process.env.REPO_NAME, 34 | state: 'all', 35 | head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, 36 | }); 37 | core.setOutput('RELEASE_NOTES', pulls[0].body); 38 | env: 39 | GITHUB_TOKEN: ${{ inputs.token }} 40 | REPO_OWNER: ${{ inputs.repo_owner }} 41 | REPO_NAME: ${{ inputs.repo_name }} 42 | VERSION: ${{ inputs.version }} 43 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Auth0 - JWT-Decode 6 | 7 | 8 |

Decoded:

9 |
10 |
11 |
12 |
13 | 20 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /.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/jwt-decode#readme) and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have searched the [issues](https://github.com/auth0/jwt-decode/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 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export interface JwtDecodeOptions { 2 | header?: boolean; 3 | } 4 | 5 | export interface JwtHeader { 6 | typ?: string; 7 | alg?: string; 8 | kid?: string; 9 | } 10 | 11 | export interface JwtPayload { 12 | iss?: string; 13 | sub?: string; 14 | aud?: string[] | string; 15 | exp?: number; 16 | nbf?: number; 17 | iat?: number; 18 | jti?: string; 19 | } 20 | 21 | export class InvalidTokenError extends Error {} 22 | 23 | InvalidTokenError.prototype.name = "InvalidTokenError"; 24 | 25 | function b64DecodeUnicode(str: string) { 26 | return decodeURIComponent( 27 | atob(str).replace(/(.)/g, (m, p) => { 28 | let code = (p as string).charCodeAt(0).toString(16).toUpperCase(); 29 | if (code.length < 2) { 30 | code = "0" + code; 31 | } 32 | return "%" + code; 33 | }), 34 | ); 35 | } 36 | 37 | function base64UrlDecode(str: string) { 38 | let output = str.replace(/-/g, "+").replace(/_/g, "/"); 39 | switch (output.length % 4) { 40 | case 0: 41 | break; 42 | case 2: 43 | output += "=="; 44 | break; 45 | case 3: 46 | output += "="; 47 | break; 48 | default: 49 | throw new Error("base64 string is not of the correct length"); 50 | } 51 | 52 | try { 53 | return b64DecodeUnicode(output); 54 | } catch (err) { 55 | return atob(output); 56 | } 57 | } 58 | 59 | export function jwtDecode( 60 | token: string, 61 | options: JwtDecodeOptions & { header: true }, 62 | ): T; 63 | export function jwtDecode(token: string, options?: JwtDecodeOptions): T; 64 | export function jwtDecode( 65 | token: string, 66 | options?: JwtDecodeOptions, 67 | ): T { 68 | if (typeof token !== "string") { 69 | throw new InvalidTokenError("Invalid token specified: must be a string"); 70 | } 71 | 72 | options ||= {}; 73 | 74 | const pos = options.header === true ? 0 : 1; 75 | const part = token.split(".")[pos]; 76 | 77 | if (typeof part !== "string") { 78 | throw new InvalidTokenError(`Invalid token specified: missing part #${pos + 1}`); 79 | } 80 | 81 | let decoded: string; 82 | try { 83 | decoded = base64UrlDecode(part); 84 | } catch (e) { 85 | throw new InvalidTokenError( 86 | `Invalid token specified: invalid base64 for part #${pos + 1} (${(e as Error).message})`, 87 | ); 88 | } 89 | 90 | try { 91 | return JSON.parse(decoded) as T; 92 | } catch (e) { 93 | throw new InvalidTokenError( 94 | `Invalid token specified: invalid json for part #${pos + 1} (${(e as Error).message})`, 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.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/jwt-decode#readme) and have not found a suitable solution or answer. 17 | required: true 18 | - label: I have searched the [issues](https://github.com/auth0/jwt-decode/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: jwt-decode version 57 | validations: 58 | required: true 59 | 60 | - type: dropdown 61 | id: environment-browser 62 | attributes: 63 | label: Which browsers have you tested in? 64 | multiple: true 65 | options: 66 | - Chrome 67 | - Edge 68 | - Safari 69 | - Firefox 70 | - Opera 71 | - IE 72 | - Other 73 | validations: 74 | required: true 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwt-decode", 3 | "version": "4.0.0", 4 | "description": "Decode JWT tokens, mostly useful for browser applications.", 5 | "type": "module", 6 | "main": "build/cjs/index.js", 7 | "module": "build/esm/index.js", 8 | "types": "build/cjs/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": { 12 | "types": "./build/esm/index.d.ts", 13 | "default": "./build/esm/index.js" 14 | }, 15 | "require": { 16 | "types": "./build/cjs/index.d.ts", 17 | "default": "./build/cjs/index.js" 18 | } 19 | } 20 | }, 21 | "keywords": [ 22 | "jwt", 23 | "browser" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/auth0/jwt-decode" 28 | }, 29 | "url": "https://github.com/auth0/jwt-decode/issues", 30 | "homepage": "https://github.com/auth0/jwt-decode#readme", 31 | "scripts": { 32 | "dev": "concurrently --kill-others \"npm run build:watch\" \"npm run dev:server\"", 33 | "dev:server": "browser-sync start --config bs-config.json", 34 | "prebuild": "shx rm -rf ./build && shx mkdir -p ./build/cjs && shx echo '{\"type\": \"commonjs\"}'> build/cjs/package.json", 35 | "build": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json", 36 | "build:watch": "npm run build -- --watch", 37 | "lint": "eslint .", 38 | "lint:package": "publint", 39 | "test": "npm run test:node && npm run test:browser", 40 | "test:node": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --coverage", 41 | "test:browser": "NODE_OPTIONS='--experimental-vm-modules --no-warnings' jest --coverage --testEnvironment=jsdom", 42 | "prepack": "npm run build", 43 | "prepare": "husky" 44 | }, 45 | "author": "Jose F. Romaniello ", 46 | "contributors": [ 47 | "Sam Bellen " 48 | ], 49 | "license": "MIT", 50 | "devDependencies": { 51 | "@typescript-eslint/eslint-plugin": "^7.1.1", 52 | "@typescript-eslint/parser": "^7.1.1", 53 | "browser-sync": "^3.0.2", 54 | "concurrently": "^9.2.0", 55 | "eslint": "^8.48.0", 56 | "eslint-config-prettier": "^10.1.5", 57 | "eslint-import-resolver-typescript": "^3.6.0", 58 | "eslint-plugin-import": "^2.28.1", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "husky": "^9.0.11", 61 | "jest": "^29.7.0", 62 | "jest-environment-jsdom": "^29.6.2", 63 | "lint-staged": "^16.0.0", 64 | "prettier": "^3.0.2", 65 | "publint": "^0.3.12", 66 | "shx": "^0.4.0", 67 | "ts-jest": "^29.1.1", 68 | "ts-node": "^10.9.1", 69 | "typescript": "^5.1.6" 70 | }, 71 | "files": [ 72 | "build" 73 | ], 74 | "engines": { 75 | "node": ">=18" 76 | }, 77 | "lint-staged": { 78 | "*.{js,ts}": "eslint --fix" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/npm-release.yml: -------------------------------------------------------------------------------- 1 | name: Create npm and GitHub Release 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node-version: 7 | required: true 8 | type: string 9 | require-build: 10 | default: true 11 | type: string 12 | release-directory: 13 | default: './' 14 | type: string 15 | secrets: 16 | github-token: 17 | required: true 18 | npm-token: 19 | required: true 20 | 21 | jobs: 22 | release: 23 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 24 | runs-on: ubuntu-latest 25 | environment: release 26 | 27 | steps: 28 | # Checkout the code 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | # Get the version from the branch name 34 | - id: get_version 35 | uses: ./get-version 36 | 37 | # Get the prerelease flag from the branch name 38 | - id: get_prerelease 39 | uses: ./get-prerelease 40 | with: 41 | version: ${{ steps.get_version.outputs.version }} 42 | 43 | # Get the release notes 44 | - id: get_release_notes 45 | uses: ./get-release-notes 46 | with: 47 | token: ${{ secrets.github-token }} 48 | version: ${{ steps.get_version.outputs.version }} 49 | repo_owner: ${{ github.repository_owner }} 50 | repo_name: ${{ github.event.repository.name }} 51 | 52 | # Check if the tag already exists 53 | - id: tag_exists 54 | uses: ./tag-exists 55 | with: 56 | tag: ${{ steps.get_version.outputs.version }} 57 | token: ${{ secrets.github-token }} 58 | 59 | # If the tag already exists, exit with an error 60 | - if: steps.tag_exists.outputs.exists == 'true' 61 | run: exit 1 62 | 63 | # Publish the release to our package manager 64 | - uses: ./npm-publish 65 | with: 66 | node-version: ${{ inputs.node-version }} 67 | require-build: ${{ inputs.require-build }} 68 | version: ${{ steps.get_version.outputs.version }} 69 | npm-token: ${{ secrets.npm-token }} 70 | release-directory: ${{ inputs.release-directory }} 71 | 72 | # Create a release for the tag 73 | - uses: ./release-create 74 | with: 75 | token: ${{ secrets.github-token }} 76 | name: ${{ steps.get_version.outputs.version }} 77 | body: ${{ steps.get_release_notes.outputs.release-notes }} 78 | tag: ${{ steps.get_version.outputs.version }} 79 | commit: ${{ github.sha }} 80 | prerelease: ${{ steps.get_prerelease.outputs.prerelease }} -------------------------------------------------------------------------------- /lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import { jwtDecode, InvalidTokenError, JwtPayload } from "./index.js"; 2 | import { describe, expect, it } from "@jest/globals"; 3 | 4 | const token = 5 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJleHAiOjEzOTMyODY4OTMsImlhdCI6MTM5MzI2ODg5M30.4-iaDojEVl0pJQMjrbM1EzUIfAZgsbK_kgnVyVxFSVo"; 6 | 7 | describe("jwt-decode", () => { 8 | it("should return default and custom claims", () => { 9 | const decoded = jwtDecode(token); 10 | expect(decoded.exp).toEqual(1393286893); 11 | expect(decoded.iat).toEqual(1393268893); 12 | expect(decoded.foo).toEqual("bar"); 13 | }); 14 | 15 | it("should return header information", () => { 16 | const decoded = jwtDecode(token, { header: true }); 17 | expect(decoded.typ).toEqual("JWT"); 18 | expect(decoded.alg).toEqual("HS256"); 19 | }); 20 | 21 | it("should work with utf8 tokens", () => { 22 | const utf8Token = 23 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9zw6kiLCJpYXQiOjE0MjU2NDQ5NjZ9.1CfFtdGUPs6q8kT3OGQSVlhEMdbuX0HfNSqum0023a0"; 24 | const decoded = jwtDecode(utf8Token); 25 | expect(decoded.name).toEqual("José"); 26 | }); 27 | 28 | it("should work with binary tokens", () => { 29 | const binaryToken = 30 | "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiSm9z6SIsImlhdCI6MTQyNTY0NDk2Nn0.cpnplCBxiw7Xqz5thkqs4Mo_dymvztnI0CI4BN0d1t8"; 31 | const decoded = jwtDecode(binaryToken); 32 | expect(decoded.name).toEqual("José"); 33 | }); 34 | 35 | it("should work with double padding", () => { 36 | const utf8Token = 37 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpvc8OpIiwiaWF0IjoxNTE2MjM5MDIyfQ.7A3F5SUH2gbBSYVon5mas_Y-KCrWojorKQg7UKGVEIA"; 38 | const decoded = jwtDecode(utf8Token); 39 | expect(decoded.name).toEqual("José"); 40 | }); 41 | 42 | it("should work with single padding", () => { 43 | const utf8Token = 44 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpvc8OpZSIsImlhdCI6MTUxNjIzOTAyMn0.tbjJzDAylkKSV0_YGR5xBJBlFK01C82nZPLIcA3JX1g"; 45 | const decoded = jwtDecode(utf8Token); 46 | expect(decoded.name).toEqual("Josée"); 47 | }); 48 | 49 | it("should throw InvalidTokenError on nonstring", () => { 50 | const badToken = null; 51 | expect(() => { 52 | jwtDecode(badToken as unknown as string); 53 | }).toThrow(InvalidTokenError); 54 | }); 55 | 56 | it("should throw InvalidTokenError on string that is not a token", () => { 57 | const badToken = "fubar"; 58 | expect(() => { 59 | jwtDecode(badToken); 60 | }).toThrow(InvalidTokenError); 61 | }); 62 | 63 | it("should throw InvalidTokenErrors when token is null", () => { 64 | const badToken = null; 65 | expect(() => { 66 | jwtDecode(badToken as unknown as string, { header: true }); 67 | }).toThrow(new InvalidTokenError("Invalid token specified: must be a string")); 68 | }); 69 | 70 | it("should throw InvalidTokenErrors when missing part #1", () => { 71 | const badToken = ".FAKE_TOKEN"; 72 | expect(() => { 73 | jwtDecode(badToken, { header: true }); 74 | }).toThrow(/Invalid token specified: invalid json for part #1/); 75 | }); 76 | 77 | it("should throw InvalidTokenErrors when part #1 is not valid base64", () => { 78 | const badToken = "TOKEN"; 79 | expect(() => { 80 | jwtDecode(badToken, { header: true }); 81 | }).toThrow(/Invalid token specified: invalid base64 for part #1/); 82 | }); 83 | 84 | it("should throw InvalidTokenErrors when part #1 is not valid JSON", () => { 85 | const badToken = "FAKE.TOKEN"; 86 | expect(() => { 87 | jwtDecode(badToken, { header: true }); 88 | }).toThrow(/Invalid token specified: invalid json for part #1/); 89 | }); 90 | 91 | it("should throw InvalidTokenErrors when missing part #2", () => { 92 | const badToken = "FAKE_TOKEN"; 93 | expect(() => { 94 | jwtDecode(badToken); 95 | }).toThrow(new InvalidTokenError("Invalid token specified: missing part #2")); 96 | }); 97 | 98 | it("should throw InvalidTokenErrors when part #2 is not valid base64", () => { 99 | const badToken = "FAKE.TOKEN"; 100 | expect(() => { 101 | jwtDecode(badToken); 102 | }).toThrow(/Invalid token specified: invalid base64 for part #2/); 103 | }); 104 | 105 | it("should throw InvalidTokenErrors when part #2 is not valid JSON", () => { 106 | const badToken = "FAKE.TOKEN2"; 107 | expect(() => { 108 | jwtDecode(badToken); 109 | }).toThrow(/Invalid token specified: invalid json for part #2/); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Browser library that helps decoding JWT tokens which are Base64Url encoded](https://cdn.auth0.com/website/sdks/banners/jwt-decode-banner.png) 2 | 3 | **IMPORTANT:** This library doesn't validate the token, any well-formed JWT can be decoded. You should validate the token in your server-side logic by using something like [express-jwt](https://github.com/auth0/express-jwt), [koa-jwt](https://github.com/stiang/koa-jwt), [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer), etc. 4 | 5 | ![Release](https://img.shields.io/npm/v/jwt-decode) 6 | ![Downloads](https://img.shields.io/npm/dw/jwt-decode) 7 | [![License](https://img.shields.io/:license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) 8 | [![CircleCI](https://img.shields.io/circleci/build/github/auth0/jwt-decode)](https://circleci.com/gh/auth0/jwt-decode) 9 | 10 | :books: [Documentation](#documentation) - :rocket: [Getting Started](#getting-started) - :speech_balloon: [Feedback](#feedback) 11 | 12 | ## Documentation 13 | 14 | - [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0. 15 | 16 | ## Getting started 17 | 18 | ### Installation 19 | 20 | Install with NPM or Yarn. 21 | 22 | Run `npm install jwt-decode` or `yarn add jwt-decode` to install the library. 23 | 24 | ### Usage 25 | 26 | ```js 27 | import { jwtDecode } from "jwt-decode"; 28 | 29 | const token = "eyJ0eXAiO.../// jwt token"; 30 | const decoded = jwtDecode(token); 31 | 32 | console.log(decoded); 33 | 34 | /* prints: 35 | * { 36 | * foo: "bar", 37 | * exp: 1393286893, 38 | * iat: 1393268893 39 | * } 40 | */ 41 | 42 | // decode header by passing in options (useful for when you need `kid` to verify a JWT): 43 | const decodedHeader = jwtDecode(token, { header: true }); 44 | console.log(decodedHeader); 45 | 46 | /* prints: 47 | * { 48 | * typ: "JWT", 49 | * alg: "HS256" 50 | * } 51 | */ 52 | ``` 53 | 54 | **Note:** A falsy or malformed token will throw an `InvalidTokenError` error; see below for more information on specific errors. 55 | 56 | ## Polyfilling atob 57 | 58 | This library relies on `atob()`, which is a global function available on [all modern browsers as well as every supported node environment](https://developer.mozilla.org/en-US/docs/Web/API/atob#browser_compatibility). 59 | 60 | In order to use `jwt-decode` in an environment that has no access to `atob()` (e.g. [React Native prior to version 0.74](https://github.com/facebook/hermes/issues/1178)), ensure to provide the corresponding polyfill in your application by using [`core-js/stable/atob`](https://www.npmjs.com/package/core-js): 61 | 62 | ```js 63 | import "core-js/stable/atob"; 64 | ``` 65 | 66 | Alternatively, you can also use [`base-64`](https://www.npmjs.com/package/base-64) and polyfill `global.atob` yourself: 67 | 68 | ```js 69 | import { decode } from "base-64"; 70 | global.atob = decode; 71 | ``` 72 | 73 | ## Errors 74 | 75 | This library works with valid JSON web tokens. The basic format of these token is 76 | ``` 77 | [part1].[part2].[part3] 78 | ``` 79 | All parts are supposed to be valid base64 (url) encoded json. 80 | Depending on the `{ header: