├── .bacon.yml ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE.md └── SECURITY.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── build-scripts ├── build.js ├── license-template.txt └── maintain-banners.js ├── lib.d.ts ├── lib.js ├── package.json ├── scripts ├── ci.sh ├── lint.sh ├── publish.sh ├── semgrep.sh ├── setup.sh ├── snyk.sh └── unit.sh ├── test ├── .oidc.config.js ├── constants.js ├── integration-test │ ├── README.md │ ├── resource-server.js │ └── resources │ │ ├── testRunner.yml │ │ └── testng.xml ├── internal-ci │ └── token.spec.js ├── keys │ ├── rsa-fake.priv │ └── rsa-fake.pub ├── spec │ ├── configuration.spec.js │ ├── verify_access_token.spec.js │ └── verify_id_token.spec.js ├── types │ └── lib.spec.ts └── util.js └── yarn.lock /.bacon.yml: -------------------------------------------------------------------------------- 1 | test_suites: 2 | - name: lint 3 | script_path: '../okta-jwt-verifier-js/scripts' 4 | sort_order: '1' 5 | timeout: '60' 6 | script_name: lint 7 | criteria: MERGE 8 | queue_name: small 9 | - name: unit 10 | script_path: '../okta-jwt-verifier-js/scripts' 11 | sort_order: '2' 12 | timeout: '60' 13 | script_name: unit 14 | criteria: MERGE 15 | queue_name: small 16 | - name: ci 17 | script_path: '../okta-jwt-verifier-js/scripts' 18 | sort_order: '3' 19 | timeout: '60' 20 | script_name: ci 21 | criteria: MERGE 22 | queue_name: small 23 | - name: publish 24 | script_path: '../okta-jwt-verifier-js/scripts' 25 | sort_order: '4' 26 | timeout: '60' 27 | script_name: publish 28 | criteria: MERGE 29 | queue_name: small 30 | - name: semgrep 31 | script_path: '../okta-jwt-verifier-js/scripts' 32 | sort_order: '5' 33 | timeout: '10' 34 | script_name: semgrep 35 | criteria: MERGE 36 | queue_name: small 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "ignorePatterns": [ 7 | "test/**/*", 8 | "target/**/*", 9 | "lib.d.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:node/recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 12, 20 | "sourceType": "module" 21 | }, 22 | "plugins": [ 23 | "node", 24 | "@typescript-eslint" 25 | ], 26 | "rules": { 27 | "max-len": ["error", { "code": 120, "comments": 120 }], 28 | "max-classes-per-file": ["error", 3], 29 | "@typescript-eslint/no-var-requires": 0, 30 | "node/no-unsupported-features/es-syntax": ["error", { "ignores": ["modules"] }], 31 | "@typescript-eslint/no-unused-vars": 0, 32 | "node/no-unpublished-require": ["error", { 33 | "allowModules": ["globby", "shelljs", "chalk"] 34 | }] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug you encountered with the Okta JWT Verifier 3 | labels: [ bug ] 4 | body: 5 | - type: textarea 6 | id: problem 7 | attributes: 8 | label: Describe the bug 9 | description: | 10 | Please be as detailed as possible. This will help us address the bug in a timely manner. 11 | placeholder: What is expected to happen... What is the actual behavior? 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: repro 17 | attributes: 18 | label: Reproduction Steps? 19 | description: | 20 | Please provide as much detail as possible to help us reproduce your bug. 21 | A reproduction repo is very helpful for us as well. 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | id: sdkVersion 27 | attributes: 28 | label: SDK Versions 29 | description: | 30 | Output of `npx envinfo --system --npmPackages '{@okta/*,}' --binaries --browsers` 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: additional 36 | attributes: 37 | label: Additional Information 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Developer Forum 3 | url: https://devforum.okta.com/ 4 | about: Get help with building your applicaiton on the Okta Platform. 5 | blank_issues_enabled: false 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature for this SDK? 3 | labels: [ enhancement ] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Describe the feature request? 9 | description: | 10 | Please leave a helpful description of the feature request here. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: resources 16 | attributes: 17 | label: New or Affected Resource(s) 18 | description: | 19 | Please list the new or affected resources 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: documentation 25 | attributes: 26 | label: Provide a documentation link 27 | description: | 28 | Please provide any links to the documentation that is at 29 | https://developer.okta.com/. This will help us with this 30 | feature request. 31 | 32 | - type: textarea 33 | id: additional 34 | attributes: 35 | label: Additional Information? 36 | 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Checklist 2 | Please check if your PR fulfills the following requirements: 3 | 4 | - [ ] The commit message follows our [guidelines](/okta/okta-oidc-js/blob/master/CONTRIBUTING.md#commit) 5 | - [ ] Tests for the changes have been added (for bug fixes / features) 6 | - [ ] Docs have been added / updated (for bug fixes / features) 7 | 8 | 9 | ## PR Type 10 | What kind of change does this PR introduce? 11 | 12 | - [ ] Bugfix 13 | - [ ] Feature 14 | - [ ] Code style update (formatting, local variables) 15 | - [ ] Refactoring (no functional changes, no api changes) 16 | - [ ] Adding Tests 17 | - [ ] Build related changes 18 | - [ ] CI related changes 19 | - [ ] Documentation changes 20 | - [ ] Other... Please describe: 21 | 22 | 23 | ## What is the current behavior? 24 | 25 | 26 | Issue Number: N/A 27 | 28 | 29 | ## What is the new behavior? 30 | 31 | 32 | ## Does this PR introduce a breaking change? 33 | - [ ] Yes 34 | - [ ] No 35 | 36 | 37 | 38 | 39 | ## Other information 40 | 41 | 42 | ## Reviewers 43 | 44 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Report a Vulnerability 4 | At Okta we take the protection of our customers’ data very seriously. If you need to report a vulnerability, please visit https://www.okta.com/vulnerability-reporting-policy/ for more information. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_STORE 4 | .envrc 5 | .yarnrc 6 | *.code-workspace 7 | lerna-debug.log 8 | yarn-debug.log* 9 | yarn-error.log* 10 | package-lock.json 11 | dist 12 | junit.xml 13 | testenv 14 | 15 | # Ignore TCK-related files in all folders 16 | okta-oidc-tck* 17 | tck-keystore.pem 18 | target 19 | 20 | # package info files are generated during build and included in final artifacts 21 | packageInfo.js 22 | packageInfo.ts 23 | coverage 24 | *.tgz 25 | .idea/ 26 | 27 | # test reports 28 | packages/*/reports/ 29 | reports/ 30 | 31 | # android/ios related for react native 32 | .gradle/ 33 | gradle/ 34 | gradlew 35 | gradlew.bat 36 | build/ 37 | local.properties 38 | *.iml 39 | xcuserdata/ 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | 5 | addons: 6 | chrome: stable 7 | 8 | services: 9 | - xvfb 10 | 11 | jdk: 12 | - default-jdk 13 | 14 | script: 15 | - bash ./scripts/travis.sh 16 | - bash ./scripts/snyk.sh 17 | 18 | install: 19 | - yarn install 20 | 21 | before_install: 22 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0 23 | - export PATH="$HOME/.yarn/bin:$PATH" 24 | - sudo apt-get update 25 | - sudo apt-get install -y libappindicator1 26 | 27 | notifications: 28 | slack: 29 | on_pull_requests: false 30 | on_success: never 31 | on_failure: always 32 | rooms: 33 | - secure: llwX53NUK+utV8UEFtzXTt5OuhXg+rx/Y3beFNBp7nwASI1k1cpYk8gHhDM6Kj14ydDGtwYd6bL4QcKWWzrA8Hc7EPpXyfgfxF7lDVxIbm62E3rSwqf2XpoNAo0EzcPvjwAGsQnydQUiQR7tR0JTzaHTX+685XreQeQyZXiIB9eWxXCaAkVCRfuoxngcAOXNw1IRMzaRt0pyYbu4qZ0RAA4bLwjlc5RqhAUllGszxX7KCSIJwAa4kXIYT/EsBC1mKeyCdvihvFYp6rPjTDL/J8w8+r9v9SVq8n6/LMcRdEpl8gOIQ3+oLzB1UcX5zWItrVqSXZ221d6z432Vu+mR5olhbcxULkfVZwtTW6tiR/5JkGxI08k5aZAmZupOjMlJN2UJtBxkAUhthCLKPXsrwU0RnW7pOeiH5CKECCsT9S06ZW2VvEJ+CZ1WOqzYtp18Kcq/VqbFqX/ubEy80m0YgnDugLGcwEUrDZFn44g+hu2B59TJMudeOTWEEk6bdy+XTNY4Ufd0uqCoHwBZQICKIXqhxMuxcRYdbFeguTzwu1R7OYagwL9S0PC/RoqrhoiN3vs/KTEVZp7yZLZautT/0cbuDQzfZzJ6RXJZzzNmrQ7Wof9lrNObJgDDdL+dZW9Umv7ELpTjqJicOlih6USjtMq+e+tBiUl/mHMFVnxbZX8= 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 4.0.1 2 | 3 | - [#53](https://github.com/okta/okta-jwt-verifier-js/pull/53) - merges https://github.com/okta/okta-jwt-verifier-js/pull/52 4 | - [#52](https://github.com/okta/okta-jwt-verifier-js/pull/52) - fix: updates `jwks-rsa` signature type 5 | 6 | # 4.0.0 7 | 8 | ### Breaking 9 | 10 | - [#47](https://github.com/okta/okta-jwt-verifier-js/pull/47) - **BREAKING CHANGE** `verifyAccessToken` and `verifyIdToken` no longer return an `njwt.Jwt` with setters (like `setIssuer` or `setSubject`). The resulting `jwt` is now frozen to prevent manipulation 11 | 12 | ### Features 13 | 14 | - [#48](https://github.com/okta/okta-jwt-verifier-js/pull/48) - feat: adds `getKeysInterceptor` option from `jwks-rsa` 15 | 16 | # 3.2.2 17 | 18 | ### Fixes 19 | 20 | - [#46](https://github.com/okta/okta-jwt-verifier-js/pull/46) - Upgrades `njwt` version to `2.0.1` to pull in [CVE-2024-34273](https://www.cve.org/CVERecord?id=CVE-2024-34273) resolution 21 | 22 | # 3.2.1 23 | 24 | ### Fixes 25 | 26 | - [#45](https://github.com/okta/okta-jwt-verifier-js/pull/45) - freezes `njwt` version 27 | 28 | # 3.2.0 29 | 30 | ### Features 31 | 32 | - [#41](https://github.com/okta/okta-jwt-verifier-js/pull/41) - adds jwt `aud` as array support 33 | - resolves [#40](https://github.com/okta/okta-jwt-verifier-js/pull/40) 34 | 35 | # 3.1.0 36 | 37 | ### Other 38 | 39 | - [#37](https://github.com/okta/okta-jwt-verifier-js/pull/37) - upgrades jwks-rsa dependencies 40 | 41 | # 3.0.1 42 | 43 | ### Fixes 44 | 45 | - [#28](https://github.com/okta/okta-jwt-verifier-js/pull/25) - Fix for deprecated option `requestAgentOptions` in favor of `requestAgent` (via jwks-rsa) 46 | - More info: https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options 47 | 48 | # 3.0.0 49 | 50 | ### Breaking Changes 51 | 52 | - Increases minimum Node version to 14 53 | 54 | ### Other 55 | 56 | - [#25](https://github.com/okta/okta-jwt-verifier-js/pull/25) 57 | - Updates njwt and jwks-rsa versions to address security vulnerability in shared sub dependency (jsonwebtoken) 58 | - Resolves https://github.com/okta/okta-jwt-verifier-js/issues/21 59 | 60 | # 2.6.0 61 | 62 | ### Features 63 | 64 | - [#12](https://github.com/okta/okta-jwt-verifier-js/pull/12) - Passes requestAgentOptions through to the jwks-rsa library 65 | 66 | ### Fixes 67 | 68 | - [#8](https://github.com/okta/okta-jwt-verifier-js/pull/8) - Fixes error on `jwt.isExpired()` invocation 69 | 70 | ### Other 71 | 72 | - [#11](https://github.com/okta/okta-jwt-verifier-js/pull/11) - Updates njwt dependency to 1.2.0 for security fixes 73 | - [#10](https://github.com/okta/okta-jwt-verifier-js/pull/10) - Updates lib.d.ts 74 | 75 | # 2.3.0 76 | 77 | ### Features 78 | 79 | - [#708](https://github.com/okta/okta-oidc-js/pull/708) - Adds support for custom JWKS URI when it cannot be constructed from issuer URI 80 | 81 | # 2.2.0 82 | 83 | ### Other 84 | 85 | - [#1012](https://github.com/okta/okta-oidc-js/pull/1012) Removes @okta/configuration-validation dependency 86 | 87 | # 2.1.0 88 | 89 | ### Other 90 | 91 | - [#979](https://github.com/okta/okta-oidc-js/pull/979) - Adds TypeScript type declaration file. Configured eslint and tsd 92 | 93 | # 2.0.1 94 | 95 | ### Other 96 | 97 | - [#952](https://github.com/okta/okta-oidc-js/pull/952) - Updates configuration-validation dependency to 1.0.0 98 | - [#953](https://github.com/okta/okta-oidc-js/pull/963) - Fixes security vulnerability in jwks-rsa dependency 99 | 100 | # 2.0.0 101 | 102 | ### Features 103 | 104 | - [#951](https://github.com/okta/okta-oidc-js/pull/951) - Adds verifyIdToken() 105 | 106 | ### Breaking Changes 107 | 108 | - [#951](https://github.com/okta/okta-oidc-js/pull/951) - Verifier will throw error "No KID specified" if no KID is present in the JWT header 109 | 110 | # 1.0.1 111 | 112 | - [#935](https://github.com/okta/okta-oidc-js/pull/935) Updates jwks-rsa version for security fixes 113 | 114 | # 1.0.0 115 | ### Features 116 | - [`9d76c9f`](https://github.com/okta/okta-oidc-js/commit/9d76c9f952506d3a51bb912a87a8da592dd7201d) - Adds verifications to verifyAccessToken() [#481](https://github.com/okta/okta-oidc-js/pull/481) 117 | 118 | ### Fixes 119 | - [`2f2d39f`](https://github.com/okta/okta-oidc-js/commit/2f2d39fd27f88f43c20e5f0e568e428ce7e7ea74) - Removes check of client_id from access tokens [#477](https://github.com/okta/okta-oidc-js/pull/477) 120 | - [`0d5afa7`](https://github.com/okta/okta-oidc-js/commit/0d5afa7854d0d5653b8541ebe68de6099a841c12) - Updates dev deps to remove vulns [#484](https://github.com/okta/okta-oidc-js/pull/484) 121 | 122 | # 0.0.16 123 | 124 | ### Features 125 | 126 | - [`213e092`](https://github.com/okta/okta-oidc-js/commit/213e092c1f26d7f818a7e838c5b7eb996d9c9e3d) - Added support for an includes operator for assertClaims [#436](https://github.com/okta/okta-oidc-js/pull/436) 127 | 128 | # 0.0.15 129 | 130 | ### Fixes 131 | 132 | - [`7fc3ebf`](https://github.com/okta/okta-oidc-js/pull/450/commits/7fc3ebfde56ac0defbd6a0587d7e48edcbd80634) - Pins jkws-rsa at 1.4.0 to work around a dependency problem (see #448 ) 133 | 134 | # 0.0.14 135 | 136 | ### Other 137 | 138 | - [`2945461`](https://github.com/okta/okta-oidc-js/pull/338/commits/294546166a41173b699579d7d647ba7d5cab0764) - Updates `@okta/configuration-validation` version. 139 | 140 | # 0.0.13 141 | 142 | ### Features 143 | 144 | - [`1ae19d1`](https://github.com/okta/okta-oidc-js/pull/320/commits/1ae19d1c08ecc41a1f31ee617ea6580c6f9804d5) - Adds configuration validation for `issuer` and `clientId` when passed into the verifier. 145 | 146 | ### Other 147 | 148 | - [`3582f25`](https://github.com/okta/okta-oidc-js/pull/318/commits/3582f259cf74dbb45b6eed673065c2d3c03e9db3) - Rely on shared environment configuration from project root. 149 | - [`c37b9cf`](https://github.com/okta/okta-oidc-js/pull/326/commits/c37b9cf483e17720b233800b8b5609c3383b8167) - Updates the TCK version to support new integration tests. 150 | - [`c8b7ab5a`](https://github.com/okta/okta-oidc-js/commit/c8b7ab5aacecf5793efb6a626c0a24a78147ded9#diff-b8cfe5f7aa410fb30a335b09346dc4d2) - Migrate dependencies to project root utilizing [yarn workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). 151 | - [`6b6aca4`](https://github.com/okta/okta-oidc-js/pull/293/commits/6b6aca40787a99e021e8e06ea2f92628b9cc8855) - Migrates mocha tests to jest. 152 | - [`0a504a6`](https://github.com/okta/okta-oidc-js/pull/223/commits/0a504a6a6d91b1c7586a48623eab3d7b0a1b926c) - Add note that this library is only for NodeJS 153 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Okta Open Source Repos 2 | 3 | Thank you for your interest in contributing to Okta's Open Source Projects! Before submitting a PR, please take a moment to read over our [Contributer License Agreement](https://developer.okta.com/cla/). In certain cases, we ask that you [sign a CLA](https://developer.okta.com/sites/all/themes/developer/pdf/okta_individual_contributor_license_agreement_2016-11.pdf) before we merge your pull request. 4 | 5 | - [Commit Message Guidelines](#commit-message-guidelines) 6 | * [Git Commit Messages](#git-commit-messages) 7 | * [Template](#template) 8 | * [Template for specific package change](#template-for-specific-package-change) 9 | * [Type](#type) 10 | * [Example](#example) 11 | * [Example for specific package change](#example-for-specific-package-change) 12 | * [Breaking changes](#breaking-changes) 13 | * [Example for a breaking change](#example-for-a-breaking-change) 14 | 15 | ## Commit Message Guidelines 16 | 17 | ### Git Commit Messages 18 | 19 | We use an adapted form of [Conventional Commits](http://conventionalcommits.org/). 20 | 21 | * Use the present tense ("Adds feature" not "Added feature") 22 | * Limit the first line to 72 characters or less 23 | * Add one feature per commit. If you have multiple features, have multiple commits. 24 | 25 | ### Template 26 | 27 | : Short Description of Commit 28 | 29 | More detailed description of commit 30 | 31 | (Optional) Resolves: 32 | 33 | ### Template for specific package change 34 | 35 | []: Short Description of Commit 36 | 37 | More detailed description of commit 38 | 39 | (Optional) Resolves: 40 | 41 | ### Type 42 | Our types include: 43 | * `feat` when creating a new feature 44 | * `fix` when fixing a bug 45 | * `test` when adding tests 46 | * `refactor` when improving the format/structure of the code 47 | * `docs` when writing docs 48 | * `release` when pushing a new release 49 | * `chore` others (ex: upgrading/downgrading dependencies) 50 | 51 | 52 | ### Example 53 | 54 | docs: Updates CONTRIBUTING.md 55 | 56 | Updates Contributing.md with new emoji categories 57 | Updates Contributing.md with new template 58 | 59 | Resolves: #1234 60 | 61 | ### Example for specific package change 62 | fix[oidc-middleware]: Fixes bad bug 63 | 64 | Fixes a very bad bug in oidc-middleware 65 | 66 | Resolves: #5678 67 | 68 | ### Breaking changes 69 | 70 | * Breaking changes MUST be indicated at the very beginning of the body section of a commit. A breaking change MUST consist of the uppercase text `BREAKING CHANGE`, followed by a colon and a space. 71 | * A description MUST be provided after the `BREAKING CHANGE:`, describing what has changed about the API. 72 | 73 | ### Example for a breaking change 74 | 75 | feat: Allows provided config object to extend other configs 76 | 77 | BREAKING CHANGE: `extends` key in config file is now used for extending other config files 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Okta software accompanied by this notice is provided pursuant to the 2 | following terms: 3 | 4 | Copyright © 2015-Present, Okta, Inc. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 7 | this file except in compliance with the License. You may obtain a copy of the 8 | License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by 9 | applicable law or agreed to in writing, software distributed under the License 10 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 | KIND, either express or implied. See the License for the specific language 12 | governing permissions and limitations under the License. 13 | 14 | The Okta software accompanied by this notice has build dependencies on certain 15 | third party software licensed under separate terms ("Third Party Components") 16 | located in THIRD_PARTY_NOTICES. 17 | 18 | 19 | Apache License 20 | Version 2.0, January 2004 21 | http://www.apache.org/licenses/ 22 | 23 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 24 | 25 | 1. Definitions. 26 | 27 | "License" shall mean the terms and conditions for use, reproduction, and 28 | distribution as defined by Sections 1 through 9 of this document. 29 | 30 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 31 | owner that is granting the License. 32 | 33 | "Legal Entity" shall mean the union of the acting entity and all other entities 34 | that control, are controlled by, or are under common control with that entity. 35 | For the purposes of this definition, "control" means (i) the power, direct or 36 | indirect, to cause the direction or management of such entity, whether by 37 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 38 | outstanding shares, or (iii) beneficial ownership of such entity. 39 | 40 | You" (or "Your") shall mean an individual or Legal Entity exercising 41 | permissions granted by this License. 42 | 43 | "Source" form shall mean the preferred form for making modifications, including 44 | but not limited to software source code, documentation source, and 45 | configuration files. 46 | 47 | "Object" form shall mean any form resulting from mechanical transformation or 48 | translation of a Source form, including but not limited to compiled object 49 | code, generated documentation, and conversions to other media types. 50 | 51 | "Work" shall mean the work of authorship, whether in Source or Object form, 52 | made available under the License, as indicated by a copyright notice that is 53 | included in or attached to the work (an example is provided in the Appendix 54 | below). 55 | 56 | "Derivative Works" shall mean any work, whether in Source or Object form, that 57 | is based on (or derived from) the Work and for which the editorial revisions, 58 | annotations, elaborations, or other modifications represent, as a whole, an 59 | original work of authorship. For the purposes of this License, Derivative Works 60 | shall not include works that remain separable from, or merely link (or bind by 61 | name) to the interfaces of, the Work and Derivative Works thereof. 62 | 63 | "Contribution" shall mean any work of authorship, including the original 64 | version of the Work and any modifications or additions to that Work or 65 | Derivative Works thereof, that is intentionally submitted to Licensor for 66 | inclusion in the Work by the copyright owner or by an individual or Legal 67 | Entity authorized to submit on behalf of the copyright owner. For the purposes 68 | of this definition, "submitted" means any form of electronic, verbal, or 69 | written communication sent to the Licensor or its representatives, including 70 | but not limited to communication on electronic mailing lists, source code 71 | control systems, and issue tracking systems that are managed by, or on behalf 72 | of, the Licensor for the purpose of discussing and improving the Work, but 73 | excluding communication that is conspicuously marked or otherwise designated in 74 | writing by the copyright owner as "Not a Contribution." 75 | 76 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 77 | of whom a Contribution has been received by Licensor and subsequently 78 | incorporated within the Work. 79 | 2. Grant of Copyright License. Subject to the terms and conditions of this 80 | License, each Contributor hereby grants to You a perpetual, worldwide, 81 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 82 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 83 | sublicense, and distribute the Work and such Derivative Works in Source or 84 | Object form. 85 | 86 | 3. Grant of Patent License. Subject to the terms and conditions of this 87 | License, each Contributor hereby grants to You a perpetual, worldwide, 88 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this 89 | section) patent license to make, have made, use, offer to sell, sell, import, 90 | and otherwise transfer the Work, where such license applies only to those 91 | patent claims licensable by such Contributor that are necessarily infringed by 92 | their Contribution(s) alone or by combination of their Contribution(s) with the 93 | Work to which such Contribution(s) was submitted. If You institute patent 94 | litigation against any entity (including a cross-claim or counterclaim in a 95 | lawsuit) alleging that the Work or a Contribution incorporated within the Work 96 | constitutes direct or contributory patent infringement, then any patent 97 | licenses granted to You under this License for that Work shall terminate as of 98 | the date such litigation is filed. 99 | 100 | 4. Redistribution. You may reproduce and distribute copies of the Work or 101 | Derivative Works thereof in any medium, with or without modifications, and in 102 | Source or Object form, provided that You meet the following conditions: 103 | 104 | (a) You must give any other recipients of the Work or Derivative Works a copy 105 | of this License; and 106 | 107 | (b) You must cause any modified files to carry prominent notices stating that 108 | You changed the files; and 109 | 110 | (c) You must retain, in the Source form of any Derivative Works that You 111 | distribute, all copyright, patent, trademark, and attribution notices from the 112 | Source form of the Work, excluding those notices that do not pertain to any 113 | part of the Derivative Works; and 114 | 115 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then 116 | any Derivative Works that You distribute must include a readable copy of the 117 | attribution notices contained within such NOTICE file, excluding those notices 118 | that do not pertain to any part of the Derivative Works, in at least one of the 119 | following places: within a NOTICE text file distributed as part of the 120 | Derivative Works; within the Source form or documentation, if provided along 121 | with the Derivative Works; or, within a display generated by the Derivative 122 | Works, if and wherever such third-party notices normally appear. The contents 123 | of the NOTICE file are for informational purposes only and do not modify the 124 | License. You may add Your own attribution notices within Derivative Works that 125 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 126 | provided that such additional attribution notices cannot be construed as 127 | modifying the License. 128 | 129 | You may add Your own copyright statement to Your modifications and may provide 130 | additional or different license terms and conditions for use, reproduction, or 131 | distribution of Your modifications, or for any such Derivative Works as a 132 | whole, provided Your use, reproduction, and distribution of the Work otherwise 133 | complies with the conditions stated in this License. 134 | 135 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 136 | Contribution intentionally submitted for inclusion in the Work by You to the 137 | Licensor shall be under the terms and conditions of this License, without any 138 | additional terms or conditions. Notwithstanding the above, nothing herein shall 139 | supersede or modify the terms of any separate license agreement you may have 140 | executed with Licensor regarding such Contributions. 141 | 142 | 6. Trademarks. This License does not grant permission to use the trade names, 143 | trademarks, service marks, or product names of the Licensor, except as required 144 | for reasonable and customary use in describing the origin of the Work and 145 | reproducing the content of the NOTICE file. 146 | 147 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 148 | writing, Licensor provides the Work (and each Contributor provides its 149 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 150 | KIND, either express or implied, including, without limitation, any warranties 151 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any risks 154 | associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, whether in 157 | tort (including negligence), contract, or otherwise, unless required by 158 | applicable law (such as deliberate and grossly negligent acts) or agreed to in 159 | writing, shall any Contributor be liable to You for damages, including any 160 | direct, indirect, special, incidental, or consequential damages of any 161 | character arising as a result of this License or out of the use or inability to 162 | use the Work (including but not limited to damages for loss of goodwill, work 163 | stoppage, computer failure or malfunction, or any and all other commercial 164 | damages or losses), even if such Contributor has been advised of the 165 | possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 168 | Derivative Works thereof, You may choose to offer, and charge a fee for, 169 | acceptance of support, warranty, indemnity, or other liability obligations 170 | and/or rights consistent with this License. However, in accepting such 171 | obligations, You may act only on Your own behalf and on Your sole 172 | responsibility, not on behalf of any other Contributor, and only if You agree 173 | to indemnify, defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason of your 175 | accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following boilerplate 182 | notice, with the fields enclosed by brackets "[]" replaced with your own 183 | identifying information. (Don't include the brackets!) The text should be 184 | enclosed in the appropriate comment syntax for the file format. We also 185 | recommend that a file or class name and description of purpose be included on 186 | the same "printed page" as the copyright notice for easier identification 187 | within third-party archives. 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Okta JWT Verifier for Node.js 2 | 3 | [![npm version](https://img.shields.io/npm/v/@okta/jwt-verifier.svg?style=flat-square)](https://www.npmjs.com/package/@okta/jwt-verifier) 4 | [![build status](https://img.shields.io/travis/okta/okta-oidc-js/master.svg?style=flat-square)](https://travis-ci.org/okta/okta-oidc-js) 5 | 6 | This library verifies Okta access and ID tokens by fetching the public keys from the JWKS endpoint of the authorization server. 7 | 8 | > This library is for Node.js applications and will not compile into a front-end application. If you need to work with tokens in front-end applications, please see [okta-auth-js](https://github.com/okta/okta-auth-js). 9 | 10 | Using Express? Our [Express Resource Server Example](https://github.com/okta/samples-nodejs-express-4/tree/master/resource-server) will show you how to use this library in your Express application. 11 | 12 | ## Release Status 13 | 14 | :heavy_check_mark: The current stable major version series is: `4.x` 15 | 16 | | Version | Status | 17 | | ------- | -------------------------------- | 18 | | `4.x` | :heavy_check_mark: Stable | 19 | | `3.x` | :warning: Retiring on 2025-01-31 | 20 | | `2.x` | :x: Retired | 21 | | `1.x` | :x: Retired | 22 | | `0.x` | :x: Retired | 23 | 24 | The latest release can always be found on the [releases page][github-releases]. 25 | 26 | ## Access Tokens 27 | 28 | This library verifies Okta access tokens (issued by [Okta Custom Authorization servers](https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server)) by fetching the public keys from the JWKS endpoint of the authorization server. If the access token is valid it will be converted to a JSON object and returned to your code. 29 | 30 | You can learn about [access tokens](https://developer.okta.com/docs/reference/api/oidc/#access-token), [scopes](https://developer.okta.com/docs/reference/api/oidc/#scopes) and [claims](https://developer.okta.com/docs/reference/api/oidc/#claims) in our [OIDC and OAuth 2.0 API Referece](https://developer.okta.com/docs/reference/api/oidc/). 31 | 32 | > Okta Custom Authorization Servers require the [API Access Management](https://developer.okta.com/docs/concepts/api-access-management/) license. If you are using Okta Org Authorization Servers (which don’t require API Access Management) you can manually validate against the /introspect endpoint ( https://developer.okta.com/docs/reference/api/oidc/#introspect ). 33 | 34 | For any access token to be valid, the following are asserted: 35 | * Signature is valid (the token was signed by a private key which has a corresponding public key in the JWKS response from the authorization server). 36 | * Access token is not expired (requires local system time to be in sync with Okta, checks the `exp` claim of the access token). 37 | * The `aud` claim matches any expected `aud` claim passed to `verifyAccessToken()`. 38 | * The `iss` claim matches the issuer the verifier is constructed with. 39 | * Any custom claim assertions that have been configured. 40 | 41 | To learn more about verification cases and Okta's tokens please read [Validate Access Tokens](https://developer.okta.com/docs/guides/validate-access-tokens/go/overview/). 42 | 43 | ## ID Tokens 44 | 45 | This library verifies Okta ID tokens (issued by [Okta Custom Authorization servers](https://developer.okta.com/docs/concepts/auth-servers/#custom-authorization-server) or [Okta Org Authorization Server](https://developer.okta.com/docs/concepts/auth-servers/#org-authorization-server)) by fetching the public keys from the JWKS endpoint of the authorization server. If the token is valid it will be converted to a JSON object and returned to your code. 46 | 47 | You can learn about [ID tokens](https://developer.okta.com/docs/reference/api/oidc/#id-token), [scopes](https://developer.okta.com/docs/reference/api/oidc/#scopes) and [claims](https://developer.okta.com/docs/reference/api/oidc/#claims) in our [OIDC and OAuth 2.0 API Referece](https://developer.okta.com/docs/reference/api/oidc/). 48 | 49 | For any ID token to be valid, the following are asserted: 50 | * Signature is valid (the token was signed by a private key which has a corresponding public key in the JWKS response from the authorization server). 51 | * ID token is not expired (requires local system time to be in sync with Okta, checks the `exp` claim of the ID token). 52 | * The `aud` claim matches the expected client ID passed to `verifyIdToken()`. 53 | * The `iss` claim matches the issuer the verifier is constructed with. 54 | * The `nonce` claim matches the expected nonce. 55 | * Any custom claim assertions that have been configured. 56 | 57 | To learn more about verification cases and Okta's tokens please read [Validate ID Tokens](https://developer.okta.com/docs/guides/validate-id-tokens/overview/). 58 | 59 | ## Upgrading 60 | 61 | For information on how to upgrade between versions of the library, see UPGRADING.md 62 | 63 | ## How to use 64 | 65 | ```bash 66 | npm install --save @okta/jwt-verifier 67 | ``` 68 | 69 | Create a verifier instance, bound to the issuer (authorization server URL): 70 | 71 | ```javascript 72 | const OktaJwtVerifier = require('@okta/jwt-verifier'); 73 | 74 | const oktaJwtVerifier = new OktaJwtVerifier({ 75 | issuer: 'https://{yourOktaDomain}/oauth2/default' // required 76 | }); 77 | ``` 78 | 79 | ### Verify access tokens 80 | 81 | ```javascript 82 | oktaJwtVerifier.verifyAccessToken(accessTokenString, expectedAud) 83 | .then(jwt => { 84 | // the token is valid (per definition of 'valid' above) 85 | console.log(jwt.claims); 86 | }) 87 | .catch(err => { 88 | // a validation failed, inspect the error 89 | }); 90 | ``` 91 | 92 | The expected audience passed to `verifyAccessToken()` is required, and can be either a string (direct match) or an array of strings (the actual `aud` claim in the token must match one of the strings). 93 | 94 | ```javascript 95 | // Passing a string for expectedAud 96 | oktaJwtVerifier.verifyAccessToken(accessTokenString, 'api://default') 97 | .then(jwt => console.log('token is valid') ) 98 | .catch(err => console.warn('token failed validation') ); 99 | 100 | oktaJwtVerifier.verifyAccessToken(accessTokenString, [ 'api://special', 'api://default'] ) 101 | .then(jwt => console.log('token is valid') ) 102 | .catch(err => console.warn('token failed validation') ); 103 | ``` 104 | 105 | ### Verify ID tokens 106 | 107 | ```javascript 108 | oktaJwtVerifier.verifyIdToken(idTokenString, expectedClientId, expectedNonce) 109 | .then(jwt => { 110 | // the token is valid (per definition of 'valid' above) 111 | console.log(jwt.claims); 112 | }) 113 | .catch(err => { 114 | // a validation failed, inspect the error 115 | }); 116 | ``` 117 | 118 | The expected client ID passed to `verifyIdToken()` is required. Expected nonce value is optional and required if the claim is present in the token body. 119 | 120 | #### Example return values of `verifyIdToken` and `verifyAccessToken`: 121 | 122 | ```javascript 123 | { 124 | header: { 125 | typ: 'JWT', 126 | alg: 'RS256', 127 | kid: 'keyId' 128 | }, 129 | claims: { 130 | sub: 'sub', 131 | name: 'name', 132 | email: 'email', 133 | ver: 1, 134 | iss: 'https://foobar.org/oauth2/default', 135 | aud: 'aud', 136 | iat: 1657621175, 137 | exp: 1657624775, 138 | jti: 'jti', 139 | amr: [ 'pwd' ], 140 | idp: 'idp', 141 | nonce: 'nonce', 142 | preferred_username: 'username@foobar.org', 143 | auth_time: 1657621173, 144 | at_hash: 'at_hash' 145 | }, 146 | toString: () => 'base64-encoded token', 147 | isExpired: () => Boolean, 148 | isNotBefore: () => Boolean, 149 | ``` 150 | 151 | ```javascript 152 | { 153 | userMessage: 'Jwt is expired', 154 | jwtString: 'base64-encoded token', 155 | parsedHeader: JwtHeader { 156 | typ: 'JWT', 157 | alg: 'RS256', 158 | kid: 'keyId' 159 | }, 160 | parsedBody: 161 | ver: 1, 162 | jti: 'jti', 163 | iss: 'iss', 164 | aud: 'api://default', 165 | iat: 1657621175, 166 | exp: 1657621475, 167 | cid: 'cid', 168 | uid: 'uid', 169 | scp: [ 'openid', 'email', 'profile' ], 170 | auth_time: 1657621173, 171 | sub: 'userame@foobar.org' 172 | }, 173 | innerError: undefined 174 | } 175 | ``` 176 | 177 | 178 | ## Custom Claims Assertions 179 | 180 | For basic use cases, you can ask the verifier to assert a custom set of claims. For example, if you need to assert that this JWT was issued for a given client id: 181 | 182 | ```javascript 183 | const verifier = new OktaJwtVerifier({ 184 | issuer: 'https://{yourOktaDomain}/oauth2/default', 185 | clientId: '{clientId}' 186 | assertClaims: { 187 | cid: '{clientId}' 188 | } 189 | }); 190 | ``` 191 | 192 | Validation fails and an error is returned if the token does not have the configured claim. 193 | 194 | For more complex use cases, you can ask the verifier to assert that a claim includes one or more values. This is useful for array type claims as well as claims that have space-separated values in a string. 195 | 196 | You use the form: `.includes` in the `assertClaims` object with an array of values to validate. 197 | 198 | For example, if you want to assert that an array claim named `groups` includes (at least) `Everyone` and `Another`, you'd write code like this: 199 | 200 | ```javascript 201 | const verifier = new OktaJwtVerifier({ 202 | issuer: ISSUER, 203 | clientId: CLIENT_ID, 204 | assertClaims: { 205 | 'groups.includes': ['Everyone', 'Another'] 206 | } 207 | }); 208 | ``` 209 | 210 | If you want to assert that a space-separated string claim name `scp` includes (at least) `promos:write` and `promos:delete`, you'd write code like this: 211 | 212 | ```javascript 213 | const verifier = new OktaJwtVerifier({ 214 | issuer: ISSUER, 215 | clientId: CLIENT_ID, 216 | assertClaims: { 217 | 'scp.includes': ['promos:write', 'promos:delete'] 218 | } 219 | }); 220 | ``` 221 | 222 | The values you want to assert are always represented as an array (the right side of the expression). The claim that you're checking against (the left side of the expression) can have either an array (like `groups`) or a space-separated list in a string (like `scp`) as its value type. 223 | 224 | NOTE: Currently, `.includes` is the only supported claim operator. 225 | 226 | ## Custom JWKS URI 227 | 228 | Custom JWKS URI can be provided. It's useful when JWKS URI cannot be based on Issuer URI: 229 | 230 | ```javascript 231 | const verifier = new OktaJwtVerifier({ 232 | issuer: 'https://{yourOktaDomain}', 233 | clientId: '{clientId}', 234 | jwksUri: 'https://{yourOktaDomain}/oauth2/v1/keys' 235 | }); 236 | ``` 237 | 238 | ## Caching & Rate Limiting 239 | 240 | * By default, found keys are cached by key ID for one hour. This can be configured with the `cacheMaxAge` option for cache entries. 241 | * If a key ID is not found in the cache, the JWKs endpoint will be requested. To prevent a DoS if many not-found keys are requested, a rate limit of 10 JWKs requests per minute is enforced. This is configurable with the `jwksRequestsPerMinute` option. 242 | 243 | Here is a configuration example that shows the default values: 244 | 245 | ```javascript 246 | // All values are default files 247 | const oktaJwtVerifier = new OktaJwtVerifier({ 248 | issuer: 'https://{yourOktaDomain}/oauth2/default', 249 | clientId: '{clientId}', 250 | cacheMaxAge: 60 * 60 * 1000, // 1 hour 251 | jwksRequestsPerMinute: 10 252 | }); 253 | ``` 254 | 255 | ## Testing 256 | Set up a SPA and a Web App in your Okta org and testing environment variables by following the [Testing](https://github.com/okta/okta-oidc-js#testing) section in okta-oidc-js Monorepo's README. 257 | 258 | **NOTE:** 259 | 260 | When creating a SPA in your Okta org, please make sure all `Implicit` checks have been checked in the `General Settings -> Application -> Allowed grant types` section. 261 | 262 | Command for running unit test: 263 | ``` 264 | yarn test:unit 265 | ``` 266 | 267 | ## Contributing 268 | We welcome contributions to all of our open-source packages. Please see the [contribution guide](https://github.com/okta/okta-oidc-js/blob/master/CONTRIBUTING.md) to understand how to structure a contribution. 269 | 270 | ### Installing dependencies for contributions 271 | We use [yarn](https://yarnpkg.com) for dependency management when developing this package: 272 | ``` 273 | yarn install 274 | ``` 275 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading between versions of @okta/jwt-verifier 2 | 3 | ## Upgrading to the 1.x series 4 | 5 | ### `expectedAud` is new and required 6 | 7 | The 0.x series `verifyAccessToken(tokenString)` is replaced by `verifyAccessToken(tokenString, expectedAud)`. 8 | 9 | The `expectedAud` parameter is required, and must match the `aud` claim within the ticket. 10 | 11 | "api://default" is a common value for this claim if not set otherwise. 12 | 13 | Additional validations are made (such as the `iss` claim must match the `issuer` given to the verifier), but they should always have been true and don't require additional configuration 14 | 15 | ### The `clientId` is no longer required 16 | 17 | Access Tokens are not required to be bound to a clientId, so the requirement of passing a clientId to the `OktaJwtVerifier()` constructor has been removed. You can pass the clientId and assert that any `cid` claim matches by using the "Custom Claims Assertions" as outlined in the README. 18 | -------------------------------------------------------------------------------- /build-scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const shell = require('shelljs') 4 | const chalk = require('chalk') 5 | const fs = require('fs') 6 | 7 | const NPM_DIR = `dist` 8 | const BANNER_CMD = `yarn banners` 9 | 10 | shell.echo(`Start building...`) 11 | 12 | shell.rm(`-Rf`, `${NPM_DIR}/*`) 13 | shell.mkdir(`-p`, `./${NPM_DIR}`) 14 | 15 | // Maintain banners 16 | if (shell.exec(BANNER_CMD).code !== 0) { 17 | shell.echo(chalk.red(`Error: Maintain banners failed`)) 18 | shell.exit(1) 19 | } 20 | 21 | shell.cp(`-Rf`, [`lib.*`, `package.json`, `LICENSE`, `*.md`], `${NPM_DIR}`) 22 | 23 | shell.echo(`Modifying final package.json`) 24 | const packageJSON = JSON.parse(fs.readFileSync(`./${NPM_DIR}/package.json`)) 25 | packageJSON.private = false; 26 | packageJSON.scripts.prepare = ''; 27 | 28 | // Remove "dist/" from the entrypoint paths. 29 | ['main', 'module', 'types'].forEach(function (key) { 30 | if (packageJSON[key]) { 31 | packageJSON[key] = packageJSON[key].replace('dist/', '') 32 | } 33 | }) 34 | 35 | fs.writeFileSync(`./${NPM_DIR}/package.json`, JSON.stringify(packageJSON, null, 4)) 36 | 37 | shell.echo(chalk.green(`End building`)) 38 | -------------------------------------------------------------------------------- /build-scripts/license-template.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ -------------------------------------------------------------------------------- /build-scripts/maintain-banners.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const globby = require('globby'); 5 | 6 | const bannerSourcePath = path.join(__dirname, 'license-template.txt') 7 | const files = globby.sync(path.join(__dirname, '..', '{*.{js,ts},test/spec/*.js}')) 8 | 9 | const bannerSource = fs.readFileSync(bannerSourcePath).toString() 10 | const copyrightRegex = /(Copyright \(c\) )([0-9]+)-Present/ 11 | 12 | files.forEach(file => { 13 | const contents = fs.readFileSync(file).toString() 14 | const match = contents.match(copyrightRegex) 15 | if (!match) { 16 | return fs.writeFileSync(file, bannerSource + '\n\n' + contents) 17 | } 18 | }) -------------------------------------------------------------------------------- /lib.d.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | /* eslint-disable node/no-missing-import */ 14 | import type { JSONWebKey } from 'jwks-rsa'; 15 | import { Agent as HttpAgent } from "node:http"; 16 | import { Agent as HttpsAgent } from "node:https"; 17 | /* eslint-enable node/no-missing-import */ 18 | 19 | export = OktaJwtVerifier; 20 | 21 | declare class OktaJwtVerifier { 22 | constructor(options: OktaJwtVerifier.VerifierOptions); 23 | 24 | /** 25 | * Verify an access token 26 | * 27 | * The expected audience passed to verifyAccessToken() is required, and can be 28 | * either a string (direct match) or an array of strings (the actual aud claim 29 | * in the token must match one of the strings). 30 | */ 31 | verifyAccessToken( 32 | accessTokenString: string, 33 | expectedAudience: string | string[] 34 | ): Promise; 35 | 36 | /** 37 | * Verify ID Tokens 38 | * 39 | * The expected client ID passed to verifyIdToken() is required. Expected nonce 40 | * value is optional and required if the claim is present in the token body. 41 | */ 42 | verifyIdToken( 43 | idTokenString: string, 44 | expectedClientId: string, 45 | expectedNonce?: string 46 | ): Promise; 47 | 48 | private verifyAsPromise(tokenString: string): Promise; 49 | } 50 | 51 | declare namespace OktaJwtVerifier { 52 | interface VerifierOptions { 53 | /** 54 | * Issuer/Authorization server URL 55 | * 56 | * @example 57 | * "https://{yourOktaDomain}/oauth2/default" 58 | */ 59 | issuer: string; 60 | /** 61 | * Client ID 62 | */ 63 | clientId?: string; 64 | /** 65 | * Custom claim assertions 66 | * 67 | * For basic use cases, you can ask the verifier to assert a custom set of 68 | * claims. For example, if you need to assert that this JWT was issued for a 69 | * given client id: 70 | * 71 | * @example 72 | * ```js 73 | * const verifier = new OktaJwtVerifier({ 74 | * issuer: 'https://{yourOktaDomain}/oauth2/default', 75 | * clientId: '{clientId}' 76 | * assertClaims: { 77 | * cid: '{clientId}' 78 | * } 79 | * }); 80 | * ``` 81 | * Validation fails and an error is returned if the token does not have the configured claim. 82 | * 83 | * Read more: https://github.com/okta/okta-jwt-verifier-js#custom-claims-assertions 84 | */ 85 | assertClaims?: Record; 86 | /** 87 | * Cache time in milliseconds 88 | * 89 | * By default, found keys are cached by key ID for one hour. This can be 90 | * configured with the cacheMaxAge option for cache entries. 91 | * 92 | * Read more: https://github.com/okta/okta-jwt-verifier-js#caching--rate-limiting 93 | */ 94 | cacheMaxAge?: number; 95 | /** 96 | * Rate limit in requests per minute 97 | * 98 | * If a key ID is not found in the cache, the JWKs endpoint will be requested. 99 | * To prevent a DoS if many not-found keys are requested, a rate limit of 10 100 | * JWKs requests per minute is enforced. This is configurable with the 101 | * jwksRequestsPerMinute option. 102 | * 103 | * Read more: https://github.com/okta/okta-jwt-verifier-js#caching--rate-limiting 104 | */ 105 | jwksRequestsPerMinute?: number; 106 | /** 107 | * Custom JWKS URI 108 | * 109 | * It's useful when JWKS URI cannot be based on Issuer URI 110 | * Defaults to `${issuer}/v1/keys` 111 | * 112 | * Read more: https://github.com/okta/okta-jwt-verifier-js#custom-jwks-uri 113 | */ 114 | jwksUri?: string; 115 | 116 | /** 117 | * HttpAgent or HttpsAgent to use for requests to the JWKS endpoint. It should 118 | * conform to the `HttpAgent` interface from node's `http` module or 119 | * the `HttpsAgent` interface from node's `https` module. 120 | * 121 | * Read more: https://nodejs.org/api/http.html#class-httpagent 122 | * Agent example: https://github.com/TooTallNate/node-https-proxy-agent 123 | */ 124 | requestAgent?: HttpAgent | HttpsAgent; 125 | 126 | /** 127 | * This option is passed to the `JwksClient` constructor. Useful when wanting to load key sets from a file, env variable or external cache 128 | * Read more: https://github.com/auth0/node-jwks-rsa/blob/master/EXAMPLES.md#loading-keys-from-local-file-environment-variable-or-other-externals 129 | */ 130 | getKeysInterceptor?(): Promise; 131 | } 132 | 133 | type Algorithm = 134 | 'HS256' 135 | | 'HS384' 136 | | 'HS512' 137 | | 'RS256' 138 | | 'RS384' 139 | | 'RS512' 140 | | 'ES256' 141 | | 'ES384' 142 | | 'ES512' 143 | | 'none'; 144 | 145 | interface JwtHeader { 146 | alg: Algorithm; 147 | typ: string; 148 | kid?: string; 149 | jku?: string; 150 | x5u?: string; 151 | x5t?: string; 152 | } 153 | 154 | interface JwtClaims { 155 | iss: string; 156 | sub: string; 157 | aud: string; 158 | exp: number; 159 | nbf?: number; 160 | iat?: number; 161 | jti?: string; 162 | nonce?: string; 163 | scp?: string[]; 164 | [key: string]: unknown; 165 | } 166 | 167 | interface Jwt { 168 | claims: JwtClaims; 169 | header: JwtHeader; 170 | toString(): string; 171 | isExpired(): boolean; 172 | isNotBefore(): boolean; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const jwksClient = require('jwks-rsa'); 14 | const nJwt = require('njwt'); 15 | 16 | class ConfigurationValidationError extends Error {} 17 | 18 | const findDomainURL = 'https://bit.ly/finding-okta-domain'; 19 | const findAppCredentialsURL = 'https://bit.ly/finding-okta-app-credentials'; 20 | 21 | const assertIssuer = (issuer, testing = {}) => { 22 | const isHttps = new RegExp('^https://'); 23 | const hasDomainAdmin = /-admin.(okta|oktapreview|okta-emea).com/; 24 | const copyMessage = 'You can copy your domain from the Okta Developer ' + 25 | 'Console. Follow these instructions to find it: ' + findDomainURL; 26 | 27 | if (testing.disableHttpsCheck) { 28 | const httpsWarning = 'Warning: HTTPS check is disabled. ' + 29 | 'This allows for insecure configurations and is NOT recommended for production use.'; 30 | /* eslint-disable-next-line no-console */ 31 | console.warn(httpsWarning); 32 | } 33 | 34 | if (!issuer) { 35 | throw new ConfigurationValidationError('Your Okta URL is missing. ' + copyMessage); 36 | } else if (!testing.disableHttpsCheck && !issuer.match(isHttps)) { 37 | throw new ConfigurationValidationError( 38 | 'Your Okta URL must start with https. ' + 39 | `Current value: ${issuer}. ${copyMessage}` 40 | ); 41 | } else if (issuer.match(/{yourOktaDomain}/)) { 42 | throw new ConfigurationValidationError('Replace {yourOktaDomain} with your Okta domain. ' + copyMessage); 43 | } else if (issuer.match(hasDomainAdmin)) { 44 | throw new ConfigurationValidationError( 45 | 'Your Okta domain should not contain -admin. ' + 46 | `Current value: ${issuer}. ${copyMessage}` 47 | ); 48 | } 49 | }; 50 | 51 | const assertClientId = (clientId) => { 52 | const copyCredentialsMessage = 'You can copy it from the Okta Developer Console ' + 53 | 'in the details for the Application you created. ' + 54 | `Follow these instructions to find it: ${findAppCredentialsURL}`; 55 | 56 | if (!clientId) { 57 | throw new ConfigurationValidationError('Your client ID is missing. ' + copyCredentialsMessage); 58 | } else if (clientId.match(/{clientId}/)) { 59 | throw new ConfigurationValidationError( 60 | 'Replace {clientId} with the client ID of your Application. ' + copyCredentialsMessage); 61 | } 62 | }; 63 | 64 | class AssertedClaimsVerifier { 65 | constructor() { 66 | this.errors = []; 67 | } 68 | 69 | extractOperator(claim) { 70 | const idx = claim.indexOf('.'); 71 | if (idx >= 0) { 72 | return claim.substring(idx + 1); 73 | } 74 | return undefined; 75 | } 76 | 77 | extractClaim(claim) { 78 | const idx = claim.indexOf('.'); 79 | if (idx >= 0) { 80 | return claim.substring(0, idx); 81 | } 82 | return claim; 83 | } 84 | 85 | isValidOperator(operator) { 86 | // may support more operators in the future 87 | return !operator || operator === 'includes'; 88 | } 89 | 90 | checkAssertions(op, claim, expectedValue, actualValue) { 91 | if (!op && actualValue !== expectedValue) { 92 | this.errors.push(`claim '${claim}' value '${actualValue}' does not match expected value '${expectedValue}'`); 93 | } else if (op === 'includes' && Array.isArray(expectedValue)) { 94 | expectedValue.forEach((value) => { 95 | if (!actualValue || !actualValue.includes(value)) { 96 | this.errors.push(`claim '${claim}' value '${actualValue}' does not include expected value '${value}'`); 97 | } 98 | }); 99 | } else if (op === 'includes' && (!actualValue || !actualValue.includes(expectedValue))) { 100 | this.errors.push(`claim '${claim}' value '${actualValue}' does not include expected value '${expectedValue}'`); 101 | } 102 | } 103 | } 104 | 105 | function verifyAssertedClaims(verifier, claims) { 106 | const assertedClaimsVerifier = new AssertedClaimsVerifier(); 107 | for (const [claimName, expectedValue] of Object.entries(verifier.claimsToAssert)) { 108 | const operator = assertedClaimsVerifier.extractOperator(claimName); 109 | if (!assertedClaimsVerifier.isValidOperator(operator)) { 110 | throw new Error(`operator: '${operator}' invalid. Supported operators: 'includes'.`); 111 | } 112 | const claim = assertedClaimsVerifier.extractClaim(claimName); 113 | const actualValue = claims[claim]; 114 | assertedClaimsVerifier.checkAssertions(operator, claim, expectedValue, actualValue); 115 | } 116 | if (assertedClaimsVerifier.errors.length) { 117 | throw new Error(assertedClaimsVerifier.errors.join(', ')); 118 | } 119 | } 120 | 121 | function verifyAudience(expected, aud) { 122 | if (!expected) { 123 | throw new Error('expected audience is required'); 124 | } 125 | 126 | if (!Array.isArray(aud)) { 127 | if (Array.isArray(expected) && !expected.includes(aud)) { 128 | throw new Error(`audience claim ${aud} does not match one of the expected audiences: ${expected.join(', ')}`); 129 | } 130 | 131 | if (!Array.isArray(expected) && aud !== expected) { 132 | throw new Error(`audience claim ${aud} does not match expected audience: ${expected}`); 133 | } 134 | } else { 135 | if (Array.isArray(expected) && !(aud.some(val => expected.includes(val)))) { 136 | throw new Error(`audience claims ${aud.join(', ')} do not match one of the expected audiences: ${expected}`); 137 | } 138 | 139 | if (!Array.isArray(expected) && !aud.includes(expected)) { 140 | throw new Error(`audience claims ${aud.join(', ')} do not include expected audience: ${expected}`); 141 | } 142 | } 143 | } 144 | 145 | function verifyClientId(expected, aud) { 146 | if (!expected) { 147 | throw new Error('expected client id is required'); 148 | } 149 | 150 | assertClientId(expected); 151 | 152 | if (aud !== expected) { 153 | throw new Error(`audience claim ${aud} does not match expected client id: ${expected}`); 154 | } 155 | } 156 | 157 | function verifyIssuer(expected, issuer) { 158 | if (issuer !== expected) { 159 | throw new Error(`issuer ${issuer} does not match expected issuer: ${expected}`); 160 | } 161 | } 162 | 163 | function verifyNonce(expected, nonce) { 164 | if (nonce && !expected) { 165 | throw new Error('expected nonce is required'); 166 | } 167 | if (!nonce && expected) { 168 | throw new Error(`nonce claim is missing but expected: ${expected}`); 169 | } 170 | if (nonce && expected && nonce !== expected) { 171 | throw new Error(`nonce claim ${nonce} does not match expected nonce: ${expected}`); 172 | } 173 | } 174 | 175 | function getJwksUri(options) { 176 | return options.jwksUri ? options.jwksUri : options.issuer + '/v1/keys'; 177 | } 178 | 179 | class OktaJwtVerifier { 180 | constructor(options = {}) { 181 | // Assert configuration options exist and are well-formed (not necessarily correct!) 182 | assertIssuer(options.issuer, options.testing); 183 | if (options.clientId) { 184 | assertClientId(options.clientId); 185 | } 186 | 187 | // https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options 188 | if (options.requestAgentOptions) { 189 | // jwks-rsa no longer accepts 'requestAgentOptions' and instead requires a http(s).Agent be passed directly 190 | const msg = `\`requestAgentOptions\` has been deprecated, use \`requestAgent\` instead. 191 | For more info see https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options`; 192 | throw new ConfigurationValidationError(msg); 193 | } 194 | 195 | this.claimsToAssert = options.assertClaims || {}; 196 | this.issuer = options.issuer; 197 | this.jwksUri = getJwksUri(options); 198 | this.jwksClient = jwksClient({ 199 | jwksUri: this.jwksUri, 200 | cache: true, 201 | cacheMaxAge: options.cacheMaxAge || (60 * 60 * 1000), 202 | cacheMaxEntries: 3, 203 | jwksRequestsPerMinute: options.jwksRequestsPerMinute || 10, 204 | rateLimit: true, 205 | // https://github.com/auth0/node-jwks-rsa/blob/master/CHANGELOG.md#request-agent-options 206 | // requestAgentOptions: options.requestAgentOptions, !! DEPRECATED !! 207 | requestAgent: options.requestAgent, 208 | getKeysInterceptor: options.getKeysInterceptor, 209 | }); 210 | this.verifier = nJwt.createVerifier().setSigningAlgorithm('RS256').withKeyResolver((kid, cb) => { 211 | if (kid) { 212 | this.jwksClient.getSigningKey(kid, (err, key) => { 213 | cb(err, key && (key.publicKey || key.rsaPublicKey)); 214 | }); 215 | } else { 216 | cb('No KID specified', null); 217 | } 218 | }); 219 | } 220 | 221 | async verifyAsPromise(tokenString) { 222 | return new Promise((resolve, reject) => { 223 | // Convert to a promise 224 | this.verifier.verify(tokenString, (err, jwt) => { 225 | if (err) { 226 | return reject(err); 227 | } 228 | 229 | const oktaJwt = { 230 | header: { ...jwt.header }, 231 | claims: { ...jwt.body }, 232 | toString: () => tokenString, 233 | isExpired: () => jwt.isExpired(), 234 | isNotBefore: () => jwt.isNotBefore() 235 | }; 236 | 237 | Object.freeze(oktaJwt.header); 238 | Object.freeze(oktaJwt.claims); 239 | Object.freeze(oktaJwt); 240 | 241 | resolve(oktaJwt); 242 | }); 243 | }); 244 | } 245 | 246 | async verifyAccessToken(accessTokenString, expectedAudience) { 247 | // njwt verifies expiration and signature. 248 | // We require RS256 in the base verifier. 249 | // Remaining to verify: 250 | // - audience claim 251 | // - issuer claim 252 | // - any custom claims passed in 253 | 254 | const jwt = await this.verifyAsPromise(accessTokenString); 255 | verifyAudience(expectedAudience, jwt.claims.aud); 256 | verifyIssuer(this.issuer, jwt.claims.iss); 257 | verifyAssertedClaims(this, jwt.claims); 258 | 259 | return jwt; 260 | } 261 | 262 | async verifyIdToken(idTokenString, expectedClientId, expectedNonce) { 263 | // njwt verifies expiration and signature. 264 | // We require RS256 in the base verifier. 265 | // Remaining to verify: 266 | // - audience claim (must match client id) 267 | // - issuer claim 268 | // - nonce claim (if present) 269 | // - any custom claims passed in 270 | 271 | const jwt = await this.verifyAsPromise(idTokenString); 272 | verifyClientId(expectedClientId, jwt.claims.aud); 273 | verifyIssuer(this.issuer, jwt.claims.iss); 274 | verifyNonce(expectedNonce, jwt.claims.nonce); 275 | verifyAssertedClaims(this, jwt.claims); 276 | 277 | return jwt; 278 | } 279 | } 280 | 281 | module.exports = OktaJwtVerifier; 282 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@okta/jwt-verifier", 3 | "private": true, 4 | "version": "4.1.0", 5 | "description": "Easily validate Okta access tokens", 6 | "repository": "https://github.com/okta/okta-jwt-verifier-js", 7 | "homepage": "https://github.com/okta/okta-jwt-verifier-js", 8 | "main": "lib.js", 9 | "types": "lib.d.ts", 10 | "scripts": { 11 | "build": "node build-scripts/build.js", 12 | "banners": "node build-scripts/maintain-banners.js", 13 | "prepare": "yarn build", 14 | "test": "yarn test:unit && yarn test:integration && yarn lint", 15 | "test:integration": "../../scripts/tck.sh", 16 | "test:unit": "JEST_JUNIT_OUTPUT_FILE=./reports/unit/results.xml jest --runInBand test/spec", 17 | "test:ci": "JEST_JUNIT_OUTPUT_FILE=./reports/ci/results.xml jest test/internal-ci", 18 | "lint": "eslint . --ext .js --ext .ts && tsd" 19 | }, 20 | "keywords": [ 21 | "okta", 22 | "oidc", 23 | "OpenId Connect", 24 | "authentication", 25 | "auth", 26 | "jwt" 27 | ], 28 | "engines": { 29 | "node": ">=14" 30 | }, 31 | "jest": { 32 | "reporters": [ 33 | "default", 34 | "jest-junit" 35 | ], 36 | "testEnvironment": "node" 37 | }, 38 | "license": "Apache-2.0", 39 | "dependencies": { 40 | "jwks-rsa": "^3.1.0", 41 | "njwt": "^2.0.1" 42 | }, 43 | "resolutions": { 44 | "minimist": "^1.2.6", 45 | "minimatch": "^3.1.2", 46 | "glob-parent": "^6.0.2" 47 | }, 48 | "devDependencies": { 49 | "@typescript-eslint/eslint-plugin": "^5.51.0", 50 | "@typescript-eslint/parser": "^5.51.0", 51 | "chalk": "^4.1.2", 52 | "cors": "^2.8.4", 53 | "cross-env": "^5.1.1", 54 | "dotenv": "^10.0.0", 55 | "eslint": "^8.34.0", 56 | "eslint-plugin-node": "^11.1.0", 57 | "express": "^4.18.2", 58 | "globby": "^11.0.4", 59 | "jest": "29.5.0", 60 | "jest-junit": "^13.0.0", 61 | "nock": "^13.2.9", 62 | "node-fetch": "^2.6.7", 63 | "shelljs": "^0.8.5", 64 | "timekeeper": "^1.0.0", 65 | "tsd": "^0.19.1", 66 | "typescript": "^4.1.5" 67 | }, 68 | "tsd": { 69 | "directory": "test/types", 70 | "compilerOptions": { 71 | "skipLibCheck": true, 72 | "esModuleInterop": true, 73 | "paths": { 74 | "@okta/jwt-verifier": [ 75 | "." 76 | ] 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | source ${OKTA_HOME}/${REPO}/scripts/setup.sh 4 | 5 | export TEST_SUITE_TYPE="junit" 6 | export TEST_RESULT_FILE_DIR="${REPO}/reports/ci" 7 | 8 | export ISSUER=https://sdk-test-ok14.okta.com/oauth2/default 9 | export CLIENT_ID=0oa5dztAOmaWJ09Dm694 10 | export USERNAME=alex@acme.com 11 | get_terminus_secret "/" password PASSWORD 12 | 13 | export CI=true 14 | export DBUS_SESSION_BUS_ADDRESS=/dev/null 15 | 16 | # Run the tests 17 | if ! yarn test:ci; then 18 | echo "ci tests failed! Exiting..." 19 | exit ${TEST_FAILURE} 20 | fi 21 | 22 | echo ${TEST_SUITE_TYPE} > ${TEST_SUITE_TYPE_FILE} 23 | echo ${TEST_RESULT_FILE_DIR} > ${TEST_RESULT_FILE_DIR_FILE} 24 | exit ${PUBLISH_TYPE_AND_RESULT_DIR} 25 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | source ${OKTA_HOME}/${REPO}/scripts/setup.sh 4 | 5 | if ! yarn lint; then 6 | echo "lint failed! Exiting..." 7 | exit ${TEST_FAILURE} 8 | fi 9 | 10 | exit ${SUCCESS} 11 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | source $OKTA_HOME/$REPO/scripts/setup.sh 4 | 5 | REGISTRY="${ARTIFACTORY_URL}/api/npm/npm-topic" 6 | 7 | export TEST_SUITE_TYPE="build" 8 | 9 | # Install required dependencies 10 | export PATH="${PATH}:$(yarn global bin)" 11 | yarn global add @okta/ci-append-sha 12 | 13 | if [ -n "${action_branch}" ]; 14 | then 15 | echo "Publishing from bacon task using branch ${action_branch}" 16 | TARGET_BRANCH=${action_branch} 17 | else 18 | echo "Publishing from bacon testSuite using branch ${BRANCH}" 19 | TARGET_BRANCH=${BRANCH} 20 | fi 21 | 22 | pushd ./dist 23 | 24 | if ! ci-append-sha; then 25 | echo "ci-append-sha failed! Exiting..." 26 | exit ${FAILED_SETUP} 27 | fi 28 | 29 | ### looks like ci-append-sha is not compatible with `yarn publish` 30 | ### which expects new-version is passed via command line parameter. 31 | ### keep using npm for now 32 | npm config set @okta:registry ${REGISTRY} 33 | if ! npm publish --registry ${REGISTRY}; then 34 | echo "npm publish failed for $PACKAGE! Exiting..." 35 | exit ${PUBLISH_ARTIFACTORY_FAILURE} 36 | fi 37 | 38 | popd 39 | 40 | exit ${SUCCESS} 41 | -------------------------------------------------------------------------------- /scripts/semgrep.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | cd ${OKTA_HOME}/${REPO} 6 | 7 | if ! sast_scan; 8 | then 9 | exit ${FAILURE} 10 | fi 11 | 12 | exit ${SUCCESS} -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -xe 2 | 3 | # Install required node version 4 | export NVM_DIR="${HOME}/.nvm" 5 | if ! setup_service node v14.18.2 &> /dev/null; then 6 | echo "Failed to install node" 7 | exit ${FAILED_SETUP} 8 | fi 9 | 10 | # Use the cacert bundled with centos as okta root CA is self-signed and cause issues downloading from yarn 11 | if ! setup_service yarn 1.21.1 /etc/pki/tls/certs/ca-bundle.crt &> /dev/null; then 12 | echo "Failed to install yarn" 13 | exit ${FAILED_SETUP} 14 | fi 15 | 16 | cd ${OKTA_HOME}/${REPO} 17 | 18 | # undo permissions change on scripts/publish.sh 19 | git checkout -- scripts 20 | 21 | # ensure we're in a branch on the correct sha 22 | git checkout $BRANCH 23 | git reset --hard $SHA 24 | 25 | git config --global user.email "oktauploader@okta.com" 26 | git config --global user.name "oktauploader-okta" 27 | 28 | if ! yarn install ; then 29 | echo "yarn install failed! Exiting..." 30 | exit ${FAILED_SETUP} 31 | fi 32 | -------------------------------------------------------------------------------- /scripts/snyk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # When packages change their dependency tree, we want to run snyk monitor. This 4 | # sends the updated dependency tree to snyk for monitoring. We want to do this 5 | # when a commit has been made to master, not on PRs because the dependency tree 6 | # is likely still in flux while review is happening. 7 | # 8 | # For simplicity, this script just runs snyk monitor against all packages on 9 | # master commits, it doesn't try to figure out which packages were updated. 10 | 11 | if [[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == "false" ]]; then 12 | yarn global add snyk 13 | snyk auth $SNYK_API_TOKEN 14 | for package in `ls $PWD/packages`; 15 | do 16 | echo "snyk monitor --org=$SNYK_ORG_ID" 17 | done 18 | fi 19 | -------------------------------------------------------------------------------- /scripts/unit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | source ${OKTA_HOME}/${REPO}/scripts/setup.sh 4 | 5 | export TEST_SUITE_TYPE="junit" 6 | export TEST_RESULT_FILE_DIR="${REPO}/reports/unit" 7 | 8 | export ISSUER=https://foo.org 9 | 10 | # Run jest with "ci" flag 11 | if ! yarn test:unit --ci; then 12 | echo "unit failed! Exiting..." 13 | exit ${TEST_FAILURE} 14 | fi 15 | 16 | echo ${TEST_SUITE_TYPE} > ${TEST_SUITE_TYPE_FILE} 17 | echo ${TEST_RESULT_FILE_DIR} > ${TEST_RESULT_FILE_DIR_FILE} 18 | exit ${PUBLISH_TYPE_AND_RESULT_DIR} 19 | -------------------------------------------------------------------------------- /test/.oidc.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared configuration for all okta-oidc-js packages. 3 | * 4 | * @param {Object} overrides - (optional) Overrides specific values for the configuration object 5 | */ 6 | 7 | // Support storing environment variables in a file named "testenv" 8 | const path = require('path'); 9 | const dotenv = require('dotenv'); 10 | const fs = require('fs'); 11 | 12 | // Read environment variables from "testenv". Override environment vars if they are already set. 13 | const TESTENV = path.resolve(__dirname, '../testenv'); 14 | if (fs.existsSync(TESTENV)) { 15 | const envConfig = dotenv.parse(fs.readFileSync(TESTENV)); 16 | Object.keys(envConfig).forEach((k) => { 17 | process.env[k] = envConfig[k]; 18 | }); 19 | } 20 | process.env.CLIENT_ID = process.env.CLIENT_ID || process.env.SPA_CLIENT_ID; 21 | 22 | module.exports = (overrides = {}) => { 23 | const PORT = overrides.port || process.env.PORT || 3000; 24 | const BASE_URI = process.env.BASE_URI || `http://localhost:${PORT}`; 25 | 26 | const defaults = { 27 | ISSUER: process.env.ISSUER || 'https://{yourOktaDomain}/oauth2/default', 28 | USERNAME: process.env.USERNAME || '{username}', 29 | PASSWORD: process.env.PASSWORD || '{password}', 30 | OKTA_TESTING_DISABLEHTTPSCHECK: process.env.OKTA_TESTING_DISABLEHTTPSCHECK || false, 31 | BASE_URI, 32 | PORT 33 | }; 34 | 35 | const spaConstants = { 36 | CLIENT_ID: process.env.SPA_CLIENT_ID || process.env.CLIENT_ID || '{clientId}', 37 | REDIRECT_URI: `${BASE_URI}/implicit/callback`, 38 | ...defaults 39 | }; 40 | 41 | const webConstants = { 42 | CLIENT_ID: process.env.WEB_CLIENT_ID || process.env.CLIENT_ID || '{clientId}', 43 | CLIENT_SECRET: process.env.CLIENT_SECRET || '{clientSecret}', 44 | APP_BASE_URL: BASE_URI, 45 | ...defaults 46 | }; 47 | 48 | return { spaConstants, webConstants }; 49 | }; 50 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const Config = require('./.oidc.config.js'); 14 | 15 | module.exports = Config({ port: 8080 }).spaConstants; 16 | -------------------------------------------------------------------------------- /test/integration-test/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Prerequisite 3 | You will need java installed on your machine. 4 | We have tested using java version "1.8.0_131" 5 | 6 | ## Run integration Tests 7 | Run the following command to run all the integration tests. 8 | Ensure you're in the `jwt-verifier` base directory 9 | 10 | ``` 11 | yarn test:integration 12 | ``` 13 | -------------------------------------------------------------------------------- /test/integration-test/resource-server.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const express = require('express'); 14 | const OktaJwtVerifier = require('../../lib'); 15 | var cors = require('cors'); 16 | 17 | const MOCK_ISSUER='http://localhost:9090/oauth2/default'; 18 | const MOCK_CLIENT_ID='OOICU812'; 19 | 20 | const oktaJwtVerifier = new OktaJwtVerifier({ 21 | issuer: MOCK_ISSUER, 22 | clientId: MOCK_CLIENT_ID, 23 | assertClaims: { 24 | aud: 'api://default' 25 | }, 26 | testing: { 27 | disableHttpsCheck: true 28 | } 29 | }); 30 | 31 | /** 32 | * A simple middleware that asserts valid access tokens and sends 401 responses 33 | * if the token is not present or fails validation. If the token is valid its 34 | * contents are attached to req.jwt 35 | */ 36 | function authenticationRequired(req, res, next) { 37 | const authHeader = req.headers.authorization || ''; 38 | 39 | const match = authHeader.match(/Bearer (.+)/); 40 | 41 | if (!match) { 42 | res.set('WWW-Authenticate', 'Bearer realm=resource error="unauthorized", error_description="Bearer token not found'); 43 | return res.status(401).end(); 44 | } 45 | 46 | const accessToken = match[1]; 47 | const expectedAud = 'api://default'; 48 | 49 | return oktaJwtVerifier.verifyAccessToken(accessToken, expectedAud) 50 | .then((jwt) => { 51 | req.jwt = jwt; 52 | next(); 53 | }) 54 | .catch((err) => { 55 | const header = `Bearer realm=resource, error="unauthorized", error_description="${err.message}"`; 56 | res.set('WWW-Authenticate', header); 57 | res.status(401).send(err.message); 58 | }); 59 | } 60 | 61 | const app = express(); 62 | 63 | /** 64 | * For local testing only! Enables CORS for all domains 65 | */ 66 | app.use(cors()); 67 | 68 | /** 69 | * An example route that requires a valid access token for authentication, it 70 | * will display a boring message to the subject if the middleware successfully 71 | * validated the token. 72 | */ 73 | app.get('/', authenticationRequired, (req, res) => { 74 | res.send(`The message of the day is boring: ${req.jwt.claims.sub}`) 75 | }); 76 | 77 | /** 78 | * An example route that requires a valid access token for authentication, it 79 | * will echo the contents of the access token if the middleware successfully 80 | * validated the token. 81 | */ 82 | app.get('/secure', authenticationRequired, (req, res) => { 83 | res.json(req.jwt); 84 | }); 85 | 86 | /** 87 | * Another example route that requires a valid access token for authentication, and 88 | * print some messages for the user if they are authenticated 89 | */ 90 | app.get('/api/messages', authenticationRequired, (req, res) => { 91 | res.json({ 92 | messages: [ 93 | { 94 | date: new Date(), 95 | text: 'I am a robot.' 96 | }, 97 | { 98 | date: new Date(new Date().getTime() - 1000 * 60 * 60), 99 | text: 'Hello, world!' 100 | } 101 | ] 102 | }); 103 | }); 104 | 105 | app.listen(8080, () => { 106 | console.log('Server Ready on port 8080'); 107 | }); 108 | -------------------------------------------------------------------------------- /test/integration-test/resources/testRunner.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017-Present Okta, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Do NOT change the env section of this config file. You can NOT run tests against your own org/issuer 18 | # Tests are run against a mock server which expects specific env values 19 | scenarios: 20 | implicit-flow-local-validation: 21 | disabledTests: 22 | - wrongScopeAccessTokenTest 23 | ports: 24 | applicationPort: 8080 25 | mockPort: 9090 26 | mockHttpsPort: 9999 27 | command: node 28 | env: 29 | NODE_EXTRA_CA_CERTS: ./tck-keystore.pem 30 | args: 31 | - test/integration-test/resource-server.js 32 | -------------------------------------------------------------------------------- /test/integration-test/resources/testng.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/internal-ci/token.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const nock = require('nock'); 14 | const tk = require('timekeeper'); 15 | 16 | const constants = require('../constants') 17 | 18 | // These tests involve LIVE network requests and run in a resource-constrained CI environment 19 | const LONG_TIMEOUT = 15000; 20 | const LONG_TIMEOUT_PDV = 50000; 21 | 22 | const { 23 | getAccessToken, 24 | getIdToken, 25 | createVerifier, 26 | createToken, 27 | getKeySet 28 | } = require('../util'); 29 | 30 | // These need to be exported in the environment, from a working Okta org 31 | const ISSUER = constants.ISSUER; 32 | const CLIENT_ID = constants.CLIENT_ID; 33 | const USERNAME = constants.USERNAME; 34 | const PASSWORD = constants.PASSWORD; 35 | const REDIRECT_URI = constants.REDIRECT_URI; 36 | const NONCE = 'foo'; 37 | 38 | // Used to get an access token and id token from the AS 39 | const issuer1TokenParams = { 40 | ISSUER, 41 | CLIENT_ID, 42 | USERNAME, 43 | PASSWORD, 44 | REDIRECT_URI, 45 | NONCE 46 | }; 47 | 48 | // JWT_VERIFIER_REPO env var is only set in PDV script 49 | if (process.env.JWT_VERIFIER_REPO) describe.only('Running only PDV tests', () => { 50 | console.warn('skipping non-PDV tests'); 51 | const expectedAud = 'api://default'; 52 | const expectedClientId = CLIENT_ID; 53 | const verifier = createVerifier(); 54 | 55 | it('should allow me to verify Okta access tokens', () => { 56 | return getAccessToken(issuer1TokenParams) 57 | .then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud)) 58 | .then(jwt => { 59 | expect(jwt.claims.iss).toBe(ISSUER); 60 | }); 61 | }, LONG_TIMEOUT_PDV); 62 | 63 | it('should allow me to verify Okta ID tokens', () => { 64 | return getIdToken(issuer1TokenParams) 65 | .then(idToken => { 66 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE); 67 | }) 68 | .then(jwt => { 69 | expect(jwt.claims.iss).toBe(ISSUER); 70 | }); 71 | }, LONG_TIMEOUT_PDV); 72 | }); 73 | 74 | describe('Access token test with api call', () => { 75 | const expectedAud = 'api://default'; 76 | const verifier = createVerifier(); 77 | 78 | it('should allow me to verify Okta access tokens', () => { 79 | return getAccessToken(issuer1TokenParams) 80 | .then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud)) 81 | .then(jwt => { 82 | expect(jwt.claims.iss).toBe(ISSUER); 83 | }); 84 | }, LONG_TIMEOUT); 85 | 86 | it('should fail if the signature is invalid', () => { 87 | return getAccessToken(issuer1TokenParams) 88 | .then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud)) 89 | .then(jwt => { 90 | // Create an access token with the same claims and kid, then re-sign it with another RSA private key - this should fail 91 | const token = createToken(jwt.claims, { kid: jwt.header.kid }); 92 | return verifier.verifyAccessToken(token, expectedAud) 93 | .catch(err => expect(err.message).toBe('Signature verification failed')); 94 | }); 95 | }, LONG_TIMEOUT); 96 | 97 | it('should fail if no kid is present in the JWT header', () => { 98 | return getAccessToken(issuer1TokenParams) 99 | .then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud)) 100 | .then(jwt => { 101 | // Create an access token that does not have a kid 102 | const token = createToken(jwt.claims); 103 | return verifier.verifyAccessToken(token, expectedAud) 104 | .catch(err => expect(err.message).toBe('Error while resolving signing key for kid "undefined"')); 105 | }); 106 | }, LONG_TIMEOUT); 107 | 108 | it('should fail if the kid cannot be found', () => { 109 | return getAccessToken(issuer1TokenParams) 110 | .then(accessToken => verifier.verifyAccessToken(accessToken, expectedAud)) 111 | .then(jwt => { 112 | // Create an access token with the same claims but a kid that will not resolve 113 | const token = createToken(jwt.claims, { kid: 'foo' }); 114 | return verifier.verifyAccessToken(token, expectedAud) 115 | .catch(err => expect(err.message).toBe('Error while resolving signing key for kid "foo"')); 116 | }); 117 | }, LONG_TIMEOUT); 118 | 119 | it('should fail if the token is expired (exp)', () => { 120 | return getAccessToken(issuer1TokenParams) 121 | .then(accessToken => 122 | verifier.verifyAccessToken(accessToken, expectedAud) 123 | .then(jwt => { 124 | // Now advance time past the exp claim 125 | const now = new Date(); 126 | const then = new Date((jwt.claims.exp * 1000) + 1000); 127 | tk.travel(then); 128 | return verifier.verifyAccessToken(accessToken, expectedAud) 129 | .then(() => { 130 | throw new Error('Should have errored'); 131 | }) 132 | .catch(err => { 133 | tk.travel(now); 134 | expect(err.message).toBe('Jwt is expired'); 135 | }); 136 | })); 137 | }, LONG_TIMEOUT); 138 | 139 | it('should allow me to assert custom claims', () => { 140 | const verifier = createVerifier({ 141 | assertClaims: { 142 | cid: 'baz', 143 | foo: 'bar' 144 | } 145 | }); 146 | return getAccessToken(issuer1TokenParams) 147 | .then(accessToken => 148 | verifier.verifyAccessToken(accessToken, expectedAud) 149 | .catch(err => { 150 | // Extra debugging for an intermittent issue 151 | const result = typeof accessToken === 'string' ? 'accessToken is a string' : accessToken; 152 | expect(result).toBe('accessToken is a string'); 153 | expect(err.message).toBe( 154 | `claim 'cid' value '${CLIENT_ID}' does not match expected value 'baz', claim 'foo' value 'undefined' does not match expected value 'bar'` 155 | ); 156 | }) 157 | ); 158 | }, LONG_TIMEOUT); 159 | 160 | it('should cache the jwks for the configured amount of time', () => { 161 | const verifier = createVerifier({ 162 | cacheMaxAge: 500 163 | }); 164 | return getAccessToken(issuer1TokenParams) 165 | .then(accessToken => { 166 | nock.recorder.rec({ 167 | output_objects: true, 168 | dont_print: true 169 | }); 170 | const nockCallObjects = nock.recorder.play(); 171 | return verifier.verifyAccessToken(accessToken, expectedAud) 172 | .then(jwt => { 173 | expect(nockCallObjects.length).toBe(1); 174 | return verifier.verifyAccessToken(accessToken, expectedAud); 175 | }) 176 | .then(jwt => { 177 | expect(nockCallObjects.length).toBe(1); 178 | return new Promise((resolve, reject) => { 179 | setTimeout(() => { 180 | verifier.verifyAccessToken(accessToken, expectedAud) 181 | .then(jwt => { 182 | expect(nockCallObjects.length).toBe(2); 183 | resolve(); 184 | }) 185 | .catch(reject); 186 | }, 1000); 187 | }); 188 | }) 189 | }); 190 | }, LONG_TIMEOUT); 191 | 192 | it('should rate limit jwks endpoint requests on cache misses', () => { 193 | const verifier = createVerifier({ 194 | jwksRequestsPerMinute: 2 195 | }); 196 | return getAccessToken(issuer1TokenParams) 197 | .then((accessToken => { 198 | nock.recorder.clear(); 199 | return verifier.verifyAccessToken(accessToken, expectedAud) 200 | .then(jwt => { 201 | // Create an access token with the same claims but a kid that will not resolve 202 | const token = createToken(jwt.claims, { kid: 'foo' }); 203 | return verifier.verifyAccessToken(token, expectedAud) 204 | .catch(err => verifier.verifyAccessToken(token, expectedAud)) 205 | .catch(err => { 206 | const nockCallObjects = nock.recorder.play(); 207 | // Expect 1 request for the valid kid, and 1 request for the 2 attempts with an invalid kid 208 | expect(nockCallObjects.length).toBe(2); 209 | }); 210 | }) 211 | })); 212 | }); 213 | }); 214 | 215 | describe('ID token tests with api calls', () => { 216 | const expectedClientId = CLIENT_ID; 217 | const verifier = createVerifier(); 218 | 219 | it('should allow me to verify Okta ID tokens', () => { 220 | return getIdToken(issuer1TokenParams) 221 | .then(idToken => { 222 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE); 223 | }) 224 | .then(jwt => { 225 | expect(jwt.claims.iss).toBe(ISSUER); 226 | }); 227 | }, LONG_TIMEOUT); 228 | 229 | it('should fail if the signature is invalid', () => { 230 | return getIdToken(issuer1TokenParams) 231 | .then(idToken => verifier.verifyIdToken(idToken, expectedClientId, NONCE)) 232 | .then(jwt => { 233 | // Create an ID token with the same claims and kid, then re-sign it with another RSA private key - this should fail 234 | const token = createToken(jwt.claims, { kid: jwt.header.kid }); 235 | 236 | return verifier.verifyIdToken(token, expectedClientId, NONCE) 237 | .catch(err => expect(err.message).toBe('Signature verification failed')); 238 | }); 239 | }, LONG_TIMEOUT); 240 | 241 | it('should fail if no kid is present in the JWT header', () => { 242 | return getIdToken(issuer1TokenParams) 243 | .then(idToken => verifier.verifyIdToken(idToken, expectedClientId, NONCE)) 244 | .then(jwt => { 245 | // Create an ID token that does not have a kid 246 | const token = createToken(jwt.claims); 247 | return verifier.verifyIdToken(token, expectedClientId, NONCE) 248 | .catch(err => expect(err.message).toBe('Error while resolving signing key for kid "undefined"')); 249 | }); 250 | }, LONG_TIMEOUT); 251 | 252 | it('should fail if the kid cannot be found', () => { 253 | return getIdToken(issuer1TokenParams) 254 | .then(idToken => verifier.verifyIdToken(idToken, expectedClientId, NONCE)) 255 | .then(jwt => { 256 | // Create an ID token with the same claims but a kid that will not resolve 257 | const token = createToken(jwt.claims, { kid: 'foo' }); 258 | return verifier.verifyIdToken(token, expectedClientId, NONCE) 259 | .catch(err => expect(err.message).toBe('Error while resolving signing key for kid "foo"')); 260 | }); 261 | }, LONG_TIMEOUT); 262 | 263 | it('should fail if the token is expired (exp)', () => { 264 | return getIdToken(issuer1TokenParams) 265 | .then(idToken => 266 | verifier.verifyIdToken(idToken, expectedClientId, NONCE) 267 | .then(jwt => { 268 | // Now advance time past the exp claim 269 | const now = new Date(); 270 | const then = new Date((jwt.claims.exp * 1000) + 1000); 271 | tk.travel(then); 272 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE) 273 | .then(() => { 274 | throw new Error('Should have errored'); 275 | }) 276 | .catch(err => { 277 | tk.travel(now); 278 | expect(err.message).toBe('Jwt is expired'); 279 | }); 280 | })); 281 | }, LONG_TIMEOUT); 282 | 283 | it('should allow me to assert custom claims', () => { 284 | const verifier = createVerifier({ 285 | assertClaims: { 286 | aud: 'baz', 287 | foo: 'bar' 288 | } 289 | }); 290 | return getIdToken(issuer1TokenParams) 291 | .then(idToken => 292 | verifier.verifyIdToken(idToken, expectedClientId, NONCE) 293 | .catch(err => { 294 | // Extra debugging for an intermittent issue 295 | const result = typeof idToken === 'string' ? 'idToken is a string' : idToken; 296 | expect(result).toBe('idToken is a string'); 297 | expect(err.message).toBe( 298 | `claim 'aud' value '${CLIENT_ID}' does not match expected value 'baz', claim 'foo' value 'undefined' does not match expected value 'bar'` 299 | ); 300 | }) 301 | ); 302 | }, LONG_TIMEOUT); 303 | 304 | it('should cache the jwks for the configured amount of time', () => { 305 | const verifier = createVerifier({ 306 | cacheMaxAge: 500 307 | }); 308 | return getIdToken(issuer1TokenParams) 309 | .then(idToken => { 310 | // OKTA-435548: access token and ID token request should not interfere 311 | nock.recorder.clear(); 312 | nock.restore(); 313 | nock.recorder.rec({ 314 | output_objects: true, 315 | dont_print: true 316 | }); 317 | const nockCallObjects = nock.recorder.play(); 318 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE) 319 | .then(jwt => { 320 | expect(nockCallObjects.length).toBe(1); 321 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE); 322 | }) 323 | .then(jwt => { 324 | expect(nockCallObjects.length).toBe(1); 325 | return new Promise((resolve, reject) => { 326 | setTimeout(() => { 327 | verifier.verifyIdToken(idToken, expectedClientId, NONCE) 328 | .then(jwt => { 329 | expect(nockCallObjects.length).toBe(2); 330 | resolve(); 331 | }) 332 | .catch(reject); 333 | }, 1000); 334 | }); 335 | }) 336 | }); 337 | }, LONG_TIMEOUT); 338 | 339 | it('should rate limit jwks endpoint requests on cache misses', () => { 340 | const verifier = createVerifier({ 341 | jwksRequestsPerMinute: 2 342 | }); 343 | return getIdToken(issuer1TokenParams) 344 | .then((idToken => { 345 | nock.recorder.clear(); 346 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE) 347 | .then(jwt => { 348 | // Create an ID token with the same claims but a kid that will not resolve 349 | const token = createToken(jwt.claims, { kid: 'foo' }); 350 | return verifier.verifyIdToken(token, expectedClientId, NONCE) 351 | .catch(err => verifier.verifyIdToken(token, expectedClientId, NONCE)) 352 | .catch(err => { 353 | const nockCallObjects = nock.recorder.play(); 354 | // Expect 1 request for the valid kid, and 1 request for the 2 attempts with an invalid kid 355 | expect(nockCallObjects.length).toBe(2); 356 | }); 357 | }) 358 | })); 359 | }); 360 | 361 | it('should use keyInterceptor function', () => { 362 | const getKeysInterceptor = jest.fn(); 363 | const verifier = createVerifier({ 364 | getKeysInterceptor 365 | }); 366 | return getKeySet() 367 | .then(res => { 368 | getKeysInterceptor.mockReturnValue(res.keys); 369 | return getIdToken(issuer1TokenParams); 370 | }) 371 | .then(idToken => { 372 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE) 373 | .then(jwt => { 374 | expect(getKeysInterceptor).toHaveBeenCalled(); 375 | expect(jwt.claims.iss).toBe(ISSUER); 376 | }) 377 | }); 378 | }); 379 | 380 | it('should use keyInterceptor function, but fallback to jwks', () => { 381 | const getKeysInterceptor = jest.fn().mockReturnValue([]); 382 | const verifier = createVerifier({ 383 | getKeysInterceptor 384 | }); 385 | return getIdToken(issuer1TokenParams) 386 | .then(idToken => { 387 | return verifier.verifyIdToken(idToken, expectedClientId, NONCE) 388 | .then(jwt => { 389 | expect(getKeysInterceptor).toHaveBeenCalled(); 390 | expect(jwt.claims.iss).toBe(ISSUER); 391 | }) 392 | }); 393 | }); 394 | 395 | }, LONG_TIMEOUT); 396 | 397 | -------------------------------------------------------------------------------- /test/keys/rsa-fake.priv: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAs8PnhPERnR/pODNVLqCrdieC5PDC3xaq/n9N3vVi4ZrVLFSo 3 | 8m5UzEzcjI2DdxeTMPK6NcojBe5O1SOv5xMtippBKpXpoagwhvhgsI3CsIFlYHue 4 | b5kGvFGOaVOrFcmzYMswZqi2VkkQ/l51B7bUIGO/K2k7CG97jzgM4Q9RxH28cl03 5 | LO7QZIjHNhPR7LZ25r2K3MUZAt0ebhoyZFTa+7G0AN1SugM1/nIcqEEqFRzbWhCk 6 | dl4bTQvkBGJ9ZTLu1z02BzQIXpyYt0j4YhIyCx6XSlYwfj0WhGsmdoT3kpl9ZJz+ 7 | WcEeM5cnDRVrs3c8No/xJrSRFjw1EbZ2frl2CQIDAQABAoIBAQCdLU9DB9zUu1AZ 8 | mZiDOmxw1L84GeLpWFKOTxTxOzEH/W8iYauOvTVbpGC6bAlkmbJS/Agge+r/hoxb 9 | A+cLbdNF+vW7nHQ4qmwztvwjou91kM3o/G1dOjmPcufH5CYU2NUEbuSU/jLfYud+ 10 | 4WwGIK982noOJOY+y+sHIITyS4i8+ZObTnS/Nr8smNBWWTc6PXTE/cHLo0AglG/N 11 | y/XeFeSPkYxYUbrUeRxcX+TfYKslF9PFp7tDtTkDuL57Dyxzr3XM67weysaG+slL 12 | J2pRTpc9P1yDEsHTSi+X8yaglE6sLlTt7ZdPIFi95SLGtnVWyDsuaPP1qrRM5JSm 13 | r/CkKfq5AoGBAOJcT51FH6egHSyWUO3taogTq0I8Jvhg31a83PfmlqB+h3+27tL0 14 | fDl3xDEWHS4/DxO/EFmiGg+Zz9VAs2PtAIx3KJRNUJ08o9N8Qi1n+J7Hk1/6bsgi 15 | SniG6zqymYrZusraxmbIxZoEjXS5pNfP3z2xAkX7GorAVnO0ZFXMiJGfAoGBAMtN 16 | s2Rb3FrMQ/Ie1qgwIgXQA2TvwqDjYECTEPu4x0kprLYAHkm0TPeNMSK/mr84rN/g 17 | JK5Qs/a3XRuWUCFdRErkYoqSqMDpMTHIl2IewbIphaaLHsZI7woGLB/3KIkJDJVN 18 | M8VWdYIQpmyo1gkjlsUw7aXjC6lk7mynasYVp2dXAoGAFTwbq+FEKvF2Syx+wxM2 19 | nzVZJ4mFDl/oE7b787WKA9xa0bxTgy60SJ/Xo9MzQZdgzrVpzz7JuxTuzk6XhZRC 20 | LOswv1jRay65H6nUe3X5eMu4I5TWt6ef3NarUoJWrRPn1gfX/ORwxRYQPxb7Q9OY 21 | Wa01TWNVBhctBQWzM+lQFnkCgYBHmM2cgl1P/K/RDNs7z/erZF5NpcI9NtYm56QL 22 | gj9snKieT9xayIxygd7UBfZRcXwwO5eva+x07o5zsLP3jAkI9vVdJ9kWVwlkJuhp 23 | PbupKsZOqJ/l5LmKJjJT747u86jiy5V34cLwUzzI94ypG0d1mo2W5iatOUZeLXeE 24 | 2wthcQKBgDtVsWHYUYOdstDrVGdOrFhoxbr2n+1tNRDpdcAiTTyhFDkfMmxkk+MA 25 | WLZI6P1ybwAFfwENSB+IfivYMvUf8bkzCBwAfG2fXSCn+tm02/IbbDqSC7D2plVu 26 | bViEcinBJ+yvEYQLig2tVVcrZSs52kk4zwd7W3FSXr6Ybse4gW1/ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/keys/rsa-fake.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs8PnhPERnR/pODNVLqCr 3 | dieC5PDC3xaq/n9N3vVi4ZrVLFSo8m5UzEzcjI2DdxeTMPK6NcojBe5O1SOv5xMt 4 | ippBKpXpoagwhvhgsI3CsIFlYHueb5kGvFGOaVOrFcmzYMswZqi2VkkQ/l51B7bU 5 | IGO/K2k7CG97jzgM4Q9RxH28cl03LO7QZIjHNhPR7LZ25r2K3MUZAt0ebhoyZFTa 6 | +7G0AN1SugM1/nIcqEEqFRzbWhCkdl4bTQvkBGJ9ZTLu1z02BzQIXpyYt0j4YhIy 7 | Cx6XSlYwfj0WhGsmdoT3kpl9ZJz+WcEeM5cnDRVrs3c8No/xJrSRFjw1EbZ2frl2 8 | CQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/spec/configuration.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const OktaJwtVerifier = require('../../lib'); 14 | 15 | describe('jwt-verifier configuration validation', () => { 16 | it('should throw if no issuer is provided', () => { 17 | function createInstance() { 18 | new OktaJwtVerifier(); 19 | } 20 | expect(createInstance).toThrow(); 21 | }); 22 | 23 | it('should throw if an issuer that does not contain https is provided', () => { 24 | function createInstance() { 25 | new OktaJwtVerifier({ 26 | issuer: 'http://foo.com' 27 | }); 28 | } 29 | expect(createInstance).toThrow(); 30 | }); 31 | 32 | it('should not throw if https issuer validation is skipped', () => { 33 | jest.spyOn(console, 'warn').mockImplementation(()=>{}); // mockImplementation to stop console.warn from actually logging 34 | function createInstance() { 35 | new OktaJwtVerifier({ 36 | issuer: 'http://foo.com', 37 | testing: { 38 | disableHttpsCheck: true 39 | } 40 | }); 41 | } 42 | expect(createInstance).not.toThrow(); 43 | expect(console.warn).toBeCalledWith('Warning: HTTPS check is disabled. This allows for insecure configurations and is NOT recommended for production use.'); 44 | }); 45 | 46 | it('should throw if an issuer matching {yourOktaDomain} is provided', () => { 47 | function createInstance() { 48 | new OktaJwtVerifier({ 49 | issuer: 'https://{yourOktaDomain}' 50 | }); 51 | } 52 | expect(createInstance).toThrow(); 53 | }); 54 | 55 | it('should throw if an issuer matching -admin.okta.com is provided', () => { 56 | function createInstance() { 57 | new OktaJwtVerifier({ 58 | issuer: 'https://foo-admin.okta.com' 59 | }); 60 | } 61 | expect(createInstance).toThrow(); 62 | }); 63 | 64 | it('should throw if an issuer matching -admin.oktapreview.com is provided', () => { 65 | function createInstance() { 66 | new OktaJwtVerifier({ 67 | issuer: 'https://foo-admin.oktapreview.com' 68 | }); 69 | } 70 | expect(createInstance).toThrow(); 71 | }); 72 | 73 | it('should throw if an issuer matching -admin.okta-emea.com is provided', () => { 74 | function createInstance() { 75 | new OktaJwtVerifier({ 76 | issuer: 'https://foo-admin.okta-emea.com' 77 | }); 78 | } 79 | expect(createInstance).toThrow(); 80 | }); 81 | 82 | it('should throw if clientId matching {clientId} is provided', () => { 83 | function createInstance() { 84 | new OktaJwtVerifier({ 85 | issuer: 'https://foo', 86 | clientId: '{clientId}', 87 | }); 88 | } 89 | expect(createInstance).toThrow(); 90 | }); 91 | 92 | it('should throw if `requestAgentOptions` is passed', () => { 93 | function createInstance() { 94 | new OktaJwtVerifier({ 95 | issuer: 'https://foo', 96 | clientId: '123456', 97 | requestAgentOptions: { 98 | timeout: 10000 99 | } 100 | }); 101 | } 102 | 103 | expect(createInstance).toThrow(); 104 | }); 105 | 106 | it('should NOT throw if clientId not matching {clientId} is provided', () => { 107 | function createInstance() { 108 | new OktaJwtVerifier({ 109 | issuer: 'https://foo', 110 | clientId: '123456', 111 | }); 112 | } 113 | expect(createInstance).not.toThrow(); 114 | }); 115 | 116 | it('should return issuer-based jwks uri when custom jwksUri is not specified', () => { 117 | 118 | const verifier = new OktaJwtVerifier({ 119 | issuer: 'https://foo', 120 | clientId: '123456', 121 | }); 122 | 123 | expect(verifier.jwksUri).toEqual('https://foo/v1/keys'); 124 | }); 125 | 126 | [undefined, ''].forEach(jwksUri => 127 | it(`should return issuer-based jwks uri when jwks custom uri is ${jwksUri}`, () => { 128 | 129 | const verifier = new OktaJwtVerifier({ 130 | issuer: 'https://foo', 131 | clientId: '123456', 132 | jwksUri: jwksUri 133 | }); 134 | 135 | expect(verifier.jwksUri).toEqual('https://foo/v1/keys'); 136 | }) 137 | ); 138 | 139 | it('should return custom jwks uri when specified', () => { 140 | 141 | const customJwksUri = 'http://custom-jwks-uri/keys'; 142 | 143 | const verifier = new OktaJwtVerifier({ 144 | issuer: 'https://foo', 145 | clientId: '123456', 146 | jwksUri: customJwksUri 147 | }); 148 | 149 | expect(verifier.jwksUri).toEqual(customJwksUri); 150 | }); 151 | 152 | it('should configure getKeysInterceptor', () => { 153 | new OktaJwtVerifier({ 154 | issuer: 'https://foo', 155 | clientId: '123456', 156 | getKeysInterceptor: () => [] 157 | }); 158 | 159 | new OktaJwtVerifier({ 160 | issuer: 'https://foo', 161 | clientId: '123456', 162 | getKeysInterceptor: async () => [] 163 | }); 164 | }); 165 | 166 | }); 167 | -------------------------------------------------------------------------------- /test/spec/verify_access_token.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | 14 | const constants = require('../constants'); 15 | 16 | const { createToken, createVerifier, createCustomClaimsVerifier, rsaKeyPair } = require('../util'); 17 | 18 | // These need to be exported in the environment, from a working Okta org 19 | const ISSUER = constants.ISSUER; 20 | 21 | describe('Jwt Verifier - Verify Access Token', () => { 22 | 23 | describe('Access Token basic validation', () => { 24 | const mockKidAsKeyFetch = (verifier) => { 25 | verifier.jwksClient.getSigningKey = jest.fn( ( kid, onKeyResolve ) => { 26 | onKeyResolve(null, { publicKey: kid } ); 27 | }); 28 | }; 29 | 30 | it('fails if the signature is invalid', () => { 31 | const token = createToken({ 32 | aud: 'http://myapp.com/', 33 | iss: ISSUER, 34 | }, { 35 | kid: rsaKeyPair.wrongPublic, 36 | }); 37 | 38 | const verifier = createVerifier(); 39 | mockKidAsKeyFetch(verifier); 40 | 41 | return verifier.verifyAccessToken(token, 'http://myapp.com/') 42 | .then( () => { throw new Error('Invalid Signature was accepted'); } ) 43 | .catch( err => { 44 | expect(err.message).toBe('Signature verification failed'); 45 | }); 46 | }); 47 | 48 | it('passes if the signature is valid', () => { 49 | const token = createToken({ 50 | aud: 'http://myapp.com/', 51 | iss: ISSUER, 52 | }, { 53 | kid: rsaKeyPair.public 54 | }); 55 | 56 | const verifier = createVerifier(); 57 | mockKidAsKeyFetch(verifier); 58 | 59 | return verifier.verifyAccessToken(token, 'http://myapp.com/'); 60 | }); 61 | 62 | it('fails if iss claim does not match verifier issuer', () => { 63 | const token = createToken({ 64 | aud: 'http://myapp.com/', 65 | iss: 'not-the-issuer', 66 | }, { 67 | kid: rsaKeyPair.public // For override of key retrieval below 68 | }); 69 | 70 | const verifier = createVerifier(); 71 | mockKidAsKeyFetch(verifier); 72 | 73 | return verifier.verifyAccessToken(token, 'http://myapp.com/') 74 | .then( () => { throw new Error('invalid issuer did not throw an error'); } ) 75 | .catch( err => { 76 | expect(err.message).toBe(`issuer not-the-issuer does not match expected issuer: ${ISSUER}`); 77 | }); 78 | }); 79 | 80 | it('fails when no audience expectation is passed', () => { 81 | const token = createToken({ 82 | aud: 'http://any-aud.com/', 83 | iss: ISSUER, 84 | }, { 85 | kid: rsaKeyPair.public // For override of key retrieval below 86 | }); 87 | 88 | const verifier = createVerifier(); 89 | mockKidAsKeyFetch(verifier); 90 | 91 | return verifier.verifyAccessToken(token) 92 | .then( () => { throw new Error('expected audience should be required, but was not'); } ) 93 | .catch( err => { 94 | expect(err.message).toBe(`expected audience is required`); 95 | }); 96 | }); 97 | 98 | it('passes when given an audience matching expectation string', () => { 99 | const token = createToken({ 100 | aud: 'http://myapp.com/', 101 | iss: ISSUER, 102 | }, { 103 | kid: rsaKeyPair.public // For override of key retrieval below 104 | }); 105 | 106 | const verifier = createVerifier(); 107 | mockKidAsKeyFetch(verifier); 108 | 109 | return verifier.verifyAccessToken(token, 'http://myapp.com/'); 110 | }); 111 | 112 | it('passes when given an audience matching expectation array', () => { 113 | const token = createToken({ 114 | aud: 'http://myapp.com/', 115 | iss: ISSUER, 116 | }, { 117 | kid: rsaKeyPair.public // For override of key retrieval below 118 | }); 119 | 120 | const verifier = createVerifier(); 121 | mockKidAsKeyFetch(verifier); 122 | 123 | return verifier.verifyAccessToken(token, [ 'one', 'http://myapp.com/', 'three'] ); 124 | }); 125 | 126 | it('passes when given an audience that is an array and matches the expectation', () => { 127 | const token = createToken({ 128 | aud: ['http://myapp.com/', 'one'], 129 | iss: ISSUER, 130 | }, { 131 | kid: rsaKeyPair.public // For override of key retrieval below 132 | }); 133 | 134 | const verifier = createVerifier(); 135 | mockKidAsKeyFetch(verifier); 136 | 137 | return verifier.verifyAccessToken(token, 'http://myapp.com/'); 138 | }) 139 | 140 | it('passes when given an audience that is an array and there is a match in the expectation array', () => { 141 | const token = createToken({ 142 | aud: ['http://myapp.com/', 'one'], 143 | iss: ISSUER, 144 | }, { 145 | kid: rsaKeyPair.public // For override of key retrieval below 146 | }); 147 | 148 | const verifier = createVerifier(); 149 | mockKidAsKeyFetch(verifier); 150 | 151 | return verifier.verifyAccessToken(token, ['two', 'http://myapp.com/']); 152 | }) 153 | 154 | it('fails when given an audience that is an array and doesnt match the expectation', () => { 155 | const token = createToken({ 156 | aud: ['http://myapp.com/', 'one'], 157 | iss: ISSUER, 158 | }, { 159 | kid: rsaKeyPair.public // For override of key retrieval below 160 | }); 161 | 162 | const verifier = createVerifier(); 163 | mockKidAsKeyFetch(verifier); 164 | 165 | return verifier.verifyAccessToken(token, 'two') 166 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 167 | .catch(err => { 168 | expect(err.message).toBe(`audience claims http://myapp.com/, one do not include expected audience: two`); 169 | }); 170 | }) 171 | 172 | it('fails when given an audience that is an array and there is no match in the expectation array', () => { 173 | const token = createToken({ 174 | aud: ['http://myapp.com/', 'one'], 175 | iss: ISSUER, 176 | }, { 177 | kid: rsaKeyPair.public // For override of key retrieval below 178 | }); 179 | 180 | const verifier = createVerifier(); 181 | mockKidAsKeyFetch(verifier); 182 | 183 | return verifier.verifyAccessToken(token, ['two, three']) 184 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 185 | .catch(err => { 186 | expect(err.message).toBe(`audience claims http://myapp.com/, one do not match one of the expected audiences: two, three`); 187 | }); 188 | }) 189 | 190 | it('fails with a invalid audience when given a valid expectation', () => { 191 | const token = createToken({ 192 | aud: 'http://wrong-aud.com/', 193 | iss: ISSUER, 194 | }, { 195 | kid: rsaKeyPair.public // For override of key retrieval below 196 | }); 197 | 198 | const verifier = createVerifier(); 199 | mockKidAsKeyFetch(verifier); 200 | 201 | return verifier.verifyAccessToken(token, 'http://myapp.com/') 202 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 203 | .catch(err => { 204 | expect(err.message).toBe(`audience claim http://wrong-aud.com/ does not match expected audience: http://myapp.com/`); 205 | }); 206 | }); 207 | 208 | it('fails with a invalid audience when given an array of expectations', () => { 209 | const token = createToken({ 210 | aud: 'http://wrong-aud.com/', 211 | iss: ISSUER, 212 | }, { 213 | kid: rsaKeyPair.public // For override of key retrieval below 214 | }); 215 | 216 | const verifier = createVerifier(); 217 | mockKidAsKeyFetch(verifier); 218 | 219 | return verifier.verifyAccessToken(token, ['one', 'http://myapp.com/', 'three']) 220 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 221 | .catch(err => { 222 | expect(err.message).toBe(`audience claim http://wrong-aud.com/ does not match one of the expected audiences: one, http://myapp.com/, three`); 223 | }); 224 | }); 225 | 226 | it('fails when given an empty array of audience expectations', () => { 227 | const token = createToken({ 228 | aud: 'http://any-aud.com/', 229 | iss: ISSUER, 230 | }, { 231 | kid: rsaKeyPair.public // For override of key retrieval below 232 | }); 233 | 234 | const verifier = createVerifier(); 235 | mockKidAsKeyFetch(verifier); 236 | 237 | return verifier.verifyAccessToken(token, []) 238 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 239 | .catch(err => { 240 | expect(err.message).toBe(`audience claim http://any-aud.com/ does not match one of the expected audiences: `); 241 | }); 242 | }); 243 | }); 244 | 245 | 246 | describe('Access Token custom claim tests with stubs', () => { 247 | const otherClaims = { 248 | iss: ISSUER, 249 | aud: 'http://myapp.com/', 250 | }; 251 | 252 | const verifier = createVerifier(); 253 | 254 | it('should only allow includes operator for custom claims', () => { 255 | verifier.claimsToAssert = {'groups.blarg': 'Everyone'}; 256 | verifier.verifier = createCustomClaimsVerifier({ 257 | groups: ['Everyone', 'Another'] 258 | }, otherClaims); 259 | 260 | return verifier.verifyAccessToken('anything', otherClaims.aud) 261 | .catch(err => expect(err.message).toBe( 262 | `operator: 'blarg' invalid. Supported operators: 'includes'.` 263 | )); 264 | }); 265 | 266 | it('should succeed in asserting claims where includes is flat, claim is array', () => { 267 | verifier.claimsToAssert = {'groups.includes': 'Everyone'}; 268 | verifier.verifier = createCustomClaimsVerifier({ 269 | groups: ['Everyone', 'Another'] 270 | }, otherClaims); 271 | 272 | return verifier.verifyAccessToken('anything', otherClaims.aud) 273 | .then(jwt => expect(jwt.claims.groups).toEqual(['Everyone', 'Another'])); 274 | }); 275 | 276 | it('should succeed in asserting claims where includes is flat, claim is flat', () => { 277 | verifier.claimsToAssert = {'scp.includes': 'promos:read'}; 278 | verifier.verifier = createCustomClaimsVerifier({ 279 | scp: 'promos:read promos:write' 280 | }, otherClaims); 281 | 282 | return verifier.verifyAccessToken('anything', otherClaims.aud) 283 | .then(jwt => expect(jwt.claims.scp).toBe('promos:read promos:write')); 284 | }); 285 | 286 | it('should fail in asserting claims where includes is flat, claim is array', () => { 287 | verifier.claimsToAssert = {'groups.includes': 'Yet Another'}; 288 | verifier.verifier = createCustomClaimsVerifier({ 289 | groups: ['Everyone', 'Another'] 290 | }, otherClaims); 291 | 292 | return verifier.verifyAccessToken('anything', otherClaims.aud) 293 | .then( () => { throw new Error(`Invalid 'groups' claim was accepted`) } ) 294 | .catch(err => expect(err.message).toBe( 295 | `claim 'groups' value 'Everyone,Another' does not include expected value 'Yet Another'` 296 | )); 297 | }); 298 | 299 | it('should fail in asserting claims where includes is flat, claim is flat', () => { 300 | const expectedAud = 'http://myapp.com/'; 301 | verifier.claimsToAssert = {'scp.includes': 'promos:delete'}; 302 | verifier.verifier = createCustomClaimsVerifier({ 303 | scp: 'promos:read promos:write' 304 | }, otherClaims); 305 | 306 | return verifier.verifyAccessToken('anything', otherClaims.aud) 307 | .then( () => { throw new Error(`Invalid 'scp' claim was accepted`) } ) 308 | .catch(err => expect(err.message).toBe( 309 | `claim 'scp' value 'promos:read promos:write' does not include expected value 'promos:delete'` 310 | )); 311 | }); 312 | 313 | it('should succeed in asserting claims where includes is array, claim is array', () => { 314 | verifier.claimsToAssert = {'groups.includes': ['Everyone', 'Yet Another']}; 315 | verifier.verifier = createCustomClaimsVerifier({ 316 | groups: ['Everyone', 'Another', 'Yet Another'] 317 | }, otherClaims); 318 | 319 | return verifier.verifyAccessToken('anything', otherClaims.aud) 320 | .then(jwt => expect(jwt.claims.groups).toEqual(['Everyone', 'Another', 'Yet Another'])); 321 | }); 322 | 323 | it('should succeed in asserting claims where includes is array, claim is flat', () => { 324 | verifier.claimsToAssert = {'scp.includes': ['promos:read', 'promos:delete']}; 325 | verifier.verifier = createCustomClaimsVerifier({ 326 | scp: 'promos:read promos:write promos:delete' 327 | }, otherClaims); 328 | 329 | return verifier.verifyAccessToken('anything', otherClaims.aud) 330 | .then(jwt => expect(jwt.claims.scp).toBe('promos:read promos:write promos:delete')); 331 | }); 332 | 333 | it('should fail in asserting claims where includes is array, claim is array', () => { 334 | verifier.claimsToAssert = {'groups.includes': ['Yet Another']}; 335 | verifier.verifier = createCustomClaimsVerifier({ 336 | groups: ['Everyone', 'Another'] 337 | }, otherClaims); 338 | 339 | return verifier.verifyAccessToken('anything', otherClaims.aud) 340 | .then( () => { throw new Error(`Invalid 'groups' claim was accepted`) } ) 341 | .catch(err => expect(err.message).toBe( 342 | `claim 'groups' value 'Everyone,Another' does not include expected value 'Yet Another'` 343 | )); 344 | }); 345 | 346 | it('should fail in asserting claims where includes is array, claim is flat', () => { 347 | verifier.claimsToAssert = {'scp.includes': ['promos:delete']}; 348 | verifier.verifier = createCustomClaimsVerifier({ 349 | scp: 'promos:read promos:write' 350 | }, otherClaims); 351 | 352 | return verifier.verifyAccessToken('anything', otherClaims.aud) 353 | .then( () => { throw new Error(`Invalid 'scp' claim was accepted`) } ) 354 | .catch(err => expect(err.message).toBe( 355 | `claim 'scp' value 'promos:read promos:write' does not include expected value 'promos:delete'` 356 | )); 357 | }); 358 | }); 359 | }); 360 | -------------------------------------------------------------------------------- /test/spec/verify_id_token.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const constants = require('../constants') 14 | 15 | const { createToken, createVerifier, createCustomClaimsVerifier, rsaKeyPair } = require('../util'); 16 | 17 | // These need to be exported in the environment, from a working Okta org 18 | const ISSUER = constants.ISSUER; 19 | const CLIENT_ID = constants.CLIENT_ID; 20 | const USERNAME = constants.USERNAME; 21 | const PASSWORD = constants.PASSWORD; 22 | const REDIRECT_URI = constants.REDIRECT_URI; 23 | const NONCE = 'foo'; 24 | 25 | // Some tests makes LIVE requests using getIdToken(). These may take much longer than normal tests 26 | const LONG_TIMEOUT = 60000; 27 | 28 | // Used to get an ID token and id token from the AS 29 | const issuer1TokenParams = { 30 | ISSUER, 31 | CLIENT_ID, 32 | USERNAME, 33 | PASSWORD, 34 | REDIRECT_URI, 35 | NONCE 36 | }; 37 | 38 | describe('Jwt Verifier - Verify ID Token', () => { 39 | const mockKidAsKeyFetch = (verifier) => { 40 | verifier.jwksClient.getSigningKey = jest.fn( ( kid, onKeyResolve ) => { 41 | onKeyResolve(null, { publicKey: kid } ); 42 | }); 43 | }; 44 | describe('ID Token basic validation', () => { 45 | it('fails if the signature is invalid', () => { 46 | const token = createToken({ 47 | aud: '0oaoesxtxmPf08QHk0h7', 48 | iss: ISSUER, 49 | }, { 50 | kid: rsaKeyPair.wrongPublic, 51 | }); 52 | 53 | const verifier = createVerifier(); 54 | mockKidAsKeyFetch(verifier); 55 | 56 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7') 57 | .then( () => { throw new Error('Invalid Signature was accepted'); } ) 58 | .catch( err => { 59 | expect(err.message).toBe('Signature verification failed'); 60 | }); 61 | }); 62 | 63 | it('passes if the signature is valid', () => { 64 | const token = createToken({ 65 | aud: '0oaoesxtxmPf08QHk0h7', 66 | iss: ISSUER, 67 | }, { 68 | kid: rsaKeyPair.public 69 | }); 70 | 71 | const verifier = createVerifier(); 72 | mockKidAsKeyFetch(verifier); 73 | 74 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7'); 75 | }); 76 | it('fails if iss claim does not match verifier issuer', () => { 77 | const token = createToken({ 78 | aud: '0oaoesxtxmPf08QHk0h7', 79 | iss: 'not-the-issuer', 80 | }, { 81 | kid: rsaKeyPair.public // For override of key retrieval below 82 | }); 83 | 84 | const verifier = createVerifier(); 85 | mockKidAsKeyFetch(verifier); 86 | 87 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7') 88 | .then( () => { throw new Error('invalid issuer did not throw an error'); } ) 89 | .catch( err => { 90 | expect(err.message).toBe(`issuer not-the-issuer does not match expected issuer: ${ISSUER}`); 91 | }); 92 | }); 93 | 94 | it('fails when no audience expectation is passed', () => { 95 | const token = createToken({ 96 | aud: 'any_client_id', 97 | iss: ISSUER, 98 | }, { 99 | kid: rsaKeyPair.public // For override of key retrieval below 100 | }); 101 | 102 | const verifier = createVerifier(); 103 | mockKidAsKeyFetch(verifier); 104 | 105 | return verifier.verifyIdToken(token) 106 | .then( () => { throw new Error('expected client id should be required, but was not'); } ) 107 | .catch( err => { 108 | expect(err.message).toBe(`expected client id is required`); 109 | }); 110 | }); 111 | 112 | it('passes when given an audience matching expectation string', () => { 113 | const token = createToken({ 114 | aud: '0oaoesxtxmPf08QHk0h7', 115 | iss: ISSUER, 116 | }, { 117 | kid: rsaKeyPair.public // For override of key retrieval below 118 | }); 119 | 120 | const verifier = createVerifier(); 121 | mockKidAsKeyFetch(verifier); 122 | 123 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7'); 124 | }); 125 | 126 | it('fails with a invalid audience when given a valid expectation', () => { 127 | const token = createToken({ 128 | aud: 'wrong_client_id', 129 | iss: ISSUER, 130 | }, { 131 | kid: rsaKeyPair.public // For override of key retrieval below 132 | }); 133 | 134 | const verifier = createVerifier(); 135 | mockKidAsKeyFetch(verifier); 136 | 137 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7') 138 | .then( () => { throw new Error('Invalid audience claim was accepted') } ) 139 | .catch(err => { 140 | expect(err.message).toBe(`audience claim wrong_client_id does not match expected client id: 0oaoesxtxmPf08QHk0h7`); 141 | }); 142 | }); 143 | 144 | it('fails with a invalid client id', () => { 145 | const token = createToken({ 146 | aud: '{clientId}', 147 | iss: ISSUER, 148 | }, { 149 | kid: rsaKeyPair.public // For override of key retrieval below 150 | }); 151 | 152 | const verifier = createVerifier(); 153 | mockKidAsKeyFetch(verifier); 154 | 155 | return verifier.verifyIdToken(token, '{clientId}') 156 | .then( () => { throw new Error('Invalid client id was accepted') } ) 157 | .catch(err => { 158 | expect(err.message).toBe("Replace {clientId} with the client ID of your Application. You can copy it from the Okta Developer Console in the details for the Application you created. Follow these instructions to find it: https://bit.ly/finding-okta-app-credentials"); 159 | }); 160 | }); 161 | 162 | it('fails when no nonce expectation is passed', () => { 163 | const token = createToken({ 164 | aud: '0oaoesxtxmPf08QHk0h7', 165 | iss: ISSUER, 166 | nonce: 'foo' 167 | }, { 168 | kid: rsaKeyPair.public // For override of key retrieval below 169 | }); 170 | 171 | const verifier = createVerifier(); 172 | mockKidAsKeyFetch(verifier); 173 | 174 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7') 175 | .then( () => { throw new Error('expected nonce should be required, but was not'); } ) 176 | .catch( err => { 177 | expect(err.message).toBe(`expected nonce is required`); 178 | }); 179 | }); 180 | 181 | it('fails when an nonce expectation is passed but claim is missing', () => { 182 | const token = createToken({ 183 | aud: '0oaoesxtxmPf08QHk0h7', 184 | iss: ISSUER 185 | }, { 186 | kid: rsaKeyPair.public // For override of key retrieval below 187 | }); 188 | 189 | const verifier = createVerifier(); 190 | mockKidAsKeyFetch(verifier); 191 | 192 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7', 'some') 193 | .then( () => { throw new Error('should not pass verification'); } ) 194 | .catch( err => { 195 | expect(err.message).toBe(`nonce claim is missing but expected: some`); 196 | }); 197 | }); 198 | 199 | it('passes when given an nonce matching expectation string', () => { 200 | const token = createToken({ 201 | aud: '0oaoesxtxmPf08QHk0h7', 202 | iss: ISSUER, 203 | nonce: 'foo' 204 | }, { 205 | kid: rsaKeyPair.public // For override of key retrieval below 206 | }); 207 | 208 | const verifier = createVerifier(); 209 | mockKidAsKeyFetch(verifier); 210 | 211 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7', 'foo'); 212 | }); 213 | 214 | it('fails with an invalid nonce when given a valid expectation', () => { 215 | const token = createToken({ 216 | aud: '0oaoesxtxmPf08QHk0h7', 217 | iss: ISSUER, 218 | nonce: 'foo' 219 | }, { 220 | kid: rsaKeyPair.public // For override of key retrieval below 221 | }); 222 | 223 | const verifier = createVerifier(); 224 | mockKidAsKeyFetch(verifier); 225 | 226 | // Not valid expectation 227 | return verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7', 'bar') 228 | .then( () => { throw new Error('Invalid nonce claim was accepted') } ) 229 | .catch(err => { 230 | expect(err.message).toBe(`nonce claim foo does not match expected nonce: bar`); 231 | }) 232 | // Expectation matches claim but in different case 233 | .then( () => verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7', 'FOO') ) 234 | .then( () => { throw new Error('Invalid nonce claim was accepted') } ) 235 | .catch(err => { 236 | expect(err.message).toBe(`nonce claim foo does not match expected nonce: FOO`); 237 | }) 238 | // Value is not a string 239 | .then( () => verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7', {}) ) 240 | .then( () => { throw new Error('Invalid nonce claim was accepted') } ) 241 | .catch(err => { 242 | expect(err.message).toBe(`nonce claim foo does not match expected nonce: [object Object]`); 243 | }) 244 | }); 245 | 246 | }); 247 | 248 | 249 | describe('ID Token custom claim tests with stubs', () => { 250 | const otherClaims = { 251 | iss: ISSUER, 252 | aud: '0oaoesxtxmPf08QHk0h7', 253 | }; 254 | 255 | const verifier = createVerifier(); 256 | 257 | it('should only allow includes operator for custom claims', () => { 258 | verifier.claimsToAssert = {'groups.blarg': 'Everyone'}; 259 | verifier.verifier = createCustomClaimsVerifier({ 260 | groups: ['Everyone', 'Another'] 261 | }, otherClaims); 262 | 263 | return verifier.verifyIdToken('anything', otherClaims.aud) 264 | .catch(err => expect(err.message).toBe( 265 | `operator: 'blarg' invalid. Supported operators: 'includes'.` 266 | )); 267 | }); 268 | 269 | it('should succeed in asserting claims where includes is flat, claim is array', () => { 270 | verifier.claimsToAssert = {'groups.includes': 'Everyone'}; 271 | verifier.verifier = createCustomClaimsVerifier({ 272 | groups: ['Everyone', 'Another'] 273 | }, otherClaims); 274 | 275 | return verifier.verifyIdToken('anything', otherClaims.aud) 276 | .then(jwt => expect(jwt.claims.groups).toEqual(['Everyone', 'Another'])); 277 | }); 278 | 279 | it('should succeed in asserting claims where includes is flat, claim is flat', () => { 280 | verifier.claimsToAssert = {'scp.includes': 'promos:read'}; 281 | verifier.verifier = createCustomClaimsVerifier({ 282 | scp: 'promos:read promos:write' 283 | }, otherClaims); 284 | 285 | return verifier.verifyIdToken('anything', otherClaims.aud) 286 | .then(jwt => expect(jwt.claims.scp).toBe('promos:read promos:write')); 287 | }); 288 | 289 | it('should fail in asserting claims where includes is flat, claim is array', () => { 290 | verifier.claimsToAssert = {'groups.includes': 'Yet Another'}; 291 | verifier.verifier = createCustomClaimsVerifier({ 292 | groups: ['Everyone', 'Another'] 293 | }, otherClaims); 294 | 295 | return verifier.verifyIdToken('anything', otherClaims.aud) 296 | .then( () => { throw new Error(`Invalid 'groups' claim was accepted`) } ) 297 | .catch(err => expect(err.message).toBe( 298 | `claim 'groups' value 'Everyone,Another' does not include expected value 'Yet Another'` 299 | )); 300 | }); 301 | 302 | it('should fail in asserting claims where includes is flat, claim is flat', () => { 303 | const expectedAud = '0oaoesxtxmPf08QHk0h7'; 304 | verifier.claimsToAssert = {'scp.includes': 'promos:delete'}; 305 | verifier.verifier = createCustomClaimsVerifier({ 306 | scp: 'promos:read promos:write' 307 | }, otherClaims); 308 | 309 | return verifier.verifyIdToken('anything', otherClaims.aud) 310 | .then( () => { throw new Error(`Invalid 'scp' claim was accepted`) } ) 311 | .catch(err => expect(err.message).toBe( 312 | `claim 'scp' value 'promos:read promos:write' does not include expected value 'promos:delete'` 313 | )); 314 | }); 315 | 316 | it('should succeed in asserting claims where includes is array, claim is array', () => { 317 | verifier.claimsToAssert = {'groups.includes': ['Everyone', 'Yet Another']}; 318 | verifier.verifier = createCustomClaimsVerifier({ 319 | groups: ['Everyone', 'Another', 'Yet Another'] 320 | }, otherClaims); 321 | 322 | return verifier.verifyIdToken('anything', otherClaims.aud) 323 | .then(jwt => expect(jwt.claims.groups).toEqual(['Everyone', 'Another', 'Yet Another'])); 324 | }); 325 | 326 | it('should succeed in asserting claims where includes is array, claim is flat', () => { 327 | verifier.claimsToAssert = {'scp.includes': ['promos:read', 'promos:delete']}; 328 | verifier.verifier = createCustomClaimsVerifier({ 329 | scp: 'promos:read promos:write promos:delete' 330 | }, otherClaims); 331 | 332 | return verifier.verifyIdToken('anything', otherClaims.aud) 333 | .then(jwt => expect(jwt.claims.scp).toBe('promos:read promos:write promos:delete')); 334 | }); 335 | 336 | it('should fail in asserting claims where includes is array, claim is array', () => { 337 | verifier.claimsToAssert = {'groups.includes': ['Yet Another']}; 338 | verifier.verifier = createCustomClaimsVerifier({ 339 | groups: ['Everyone', 'Another'] 340 | }, otherClaims); 341 | 342 | return verifier.verifyIdToken('anything', otherClaims.aud) 343 | .then( () => { throw new Error(`Invalid 'groups' claim was accepted`) } ) 344 | .catch(err => expect(err.message).toBe( 345 | `claim 'groups' value 'Everyone,Another' does not include expected value 'Yet Another'` 346 | )); 347 | }); 348 | 349 | it('should fail in asserting claims where includes is array, claim is flat', () => { 350 | verifier.claimsToAssert = {'scp.includes': ['promos:delete']}; 351 | verifier.verifier = createCustomClaimsVerifier({ 352 | scp: 'promos:read promos:write' 353 | }, otherClaims); 354 | 355 | return verifier.verifyIdToken('anything', otherClaims.aud) 356 | .then( () => { throw new Error(`Invalid 'scp' claim was accepted`) } ) 357 | .catch(err => expect(err.message).toBe( 358 | `claim 'scp' value 'promos:read promos:write' does not include expected value 'promos:delete'` 359 | )); 360 | }); 361 | }); 362 | 363 | describe('Verified JWT', function () { 364 | let token; 365 | let jwt; 366 | beforeEach(async () => { 367 | token = createToken({ 368 | aud: '0oaoesxtxmPf08QHk0h7', 369 | iss: ISSUER, 370 | exp: Math.floor(Date.now() / 1000) 371 | }, { 372 | kid: rsaKeyPair.public 373 | }); 374 | 375 | const verifier = createVerifier(); 376 | mockKidAsKeyFetch(verifier); 377 | 378 | jwt = await verifier.verifyIdToken(token, '0oaoesxtxmPf08QHk0h7'); 379 | }); 380 | 381 | it('has claims accessors', () => { 382 | jest.useFakeTimers(); 383 | expect(jwt.toString()).toBe(token); 384 | expect(jwt.isExpired()).toBe(false); 385 | expect(jwt.isNotBefore()).toBe(false); 386 | jest.advanceTimersByTime((60*60*1000) + 1); 387 | expect(jwt.toString()).toBe(token); 388 | expect(jwt.isExpired()).toBe(true); // ensures jwt.isExpired() returns true/false based on real timestamp 389 | expect(jwt.isNotBefore()).toBe(false); 390 | jest.clearAllTimers(); 391 | jest.useRealTimers(); 392 | }); 393 | 394 | it('has readonly \'claims\' property', () => { 395 | jwt.claims = { aud: 'defaultAPI' }; 396 | expect(jwt.claims.aud).toEqual('0oaoesxtxmPf08QHk0h7'); 397 | expect(Object.isFrozen(jwt.header)).toBe(true); 398 | expect(Object.isFrozen(jwt.claims)).toBe(true); 399 | expect(Object.isFrozen(jwt)).toBe(true); 400 | }); 401 | 402 | it('should not allow manipulation of verified jwt', () => { 403 | expect(() => jwt.setClaim('customClaim', 'claimValue')).toThrow(); 404 | expect(() => jwt.setClaim('exp', (new Date() - 1) / 1000)).toThrow(); 405 | expect(() => jwt.setJti('foobar')).toThrow(); 406 | expect(() => jwt.setSubject('foobar')).toThrow(); 407 | expect(() => jwt.setIssuer('foobar')).toThrow(); 408 | expect(() => jwt.setIssuedAt('foobar')).toThrow(); 409 | expect(() => jwt.setExpiration('foobar')).toThrow(); 410 | expect(() => jwt.setNotBefore('foobar')).toThrow(); 411 | }); 412 | }) 413 | }); 414 | -------------------------------------------------------------------------------- /test/types/lib.spec.ts: -------------------------------------------------------------------------------- 1 | import OktaJwtVerifier from '../../lib'; 2 | import {expectType, expectError, expectAssignable, expectNotAssignable} from 'tsd'; 3 | 4 | const main = async () => { 5 | // OktaJwtVerifier constructor 6 | // issuer is required 7 | const verifier = new OktaJwtVerifier({ issuer: 'https://foo' }); 8 | expectType(verifier); 9 | // Expected error: Missing issuer 10 | expectError(new OktaJwtVerifier({ clientId: '1234' })); 11 | // With all options 12 | expectType(new OktaJwtVerifier({ 13 | issuer: 'https://foo', 14 | clientId: '1234', 15 | assertClaims: { cid: '{clientId}' }, 16 | cacheMaxAge: 1000*60*60*2, 17 | jwksRequestsPerMinute: 100 18 | })); 19 | 20 | // verifyAccessToken 21 | // Expected error: Missing expectedAudience 22 | expectError(verifier.verifyAccessToken('accessTokenString')); 23 | expectType(await verifier.verifyAccessToken('accessTokenString', 'expectedAudience')); 24 | const jwt = await verifier.verifyAccessToken('accessTokenString', [ 25 | 'expectedAudience', 26 | 'expectedAudience2', 27 | ]); 28 | 29 | // JWT 30 | expectType(jwt.claims); 31 | expectType(jwt.header); 32 | expectType(jwt.toString()); 33 | 34 | // JWT Claims 35 | expectAssignable({ 36 | jti: "AT.0mP4JKAZX1iACIT4vbEDF7LpvDVjxypPMf0D7uX39RE", 37 | iss: "https://${yourOktaDomain}/oauth2/0oacqf8qaJw56czJi0g4", 38 | aud: "https://api.example.com", 39 | sub: "00ujmkLgagxeRrAg20g3", 40 | iat: 1467145094, 41 | exp: 1467148694, 42 | cid: "nmdP1fcyvdVO11AL7ECm", 43 | uid: "00ujmkLgagxeRrAg20g3", 44 | scp: [ 45 | "openid", 46 | "email", 47 | "flights", 48 | "custom" 49 | ], 50 | custom_claim: "CustomValue" 51 | }); 52 | expectNotAssignable({ 53 | exp: 'not-a-number' 54 | }); 55 | 56 | // JWT Header 57 | expectAssignable({ 58 | alg: 'RS256', 59 | kid: "45js03w0djwedsw", 60 | typ: 'JWT' 61 | }); 62 | expectNotAssignable({ 63 | alg: 'unsupported-alg', 64 | }); 65 | 66 | // verifyIdToken 67 | // Expected error: Missing expectedClientId 68 | expectError(verifier.verifyIdToken('idTokenString')); 69 | // expectedNonce is optional 70 | expectType(await verifier.verifyIdToken('idTokenString', 'expectedClientId')); 71 | // Expected error: Invalid type for expectedClientId 72 | expectError(verifier.verifyIdToken('idTokenString', ['expectedClientId'], 'expectedNonce')); 73 | expectType(await verifier.verifyIdToken('idTokenString', 'expectedClientId', 'expectedNonce')); 74 | }; 75 | 76 | main(); 77 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2017-Present, Okta, Inc. and/or its affiliates. All rights reserved. 3 | * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") 4 | * 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 6 | * Unless required by applicable law or agreed to in writing, software 7 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 8 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | * 10 | * See the License for the specific language governing permissions and limitations under the License. 11 | */ 12 | 13 | const fs = require('fs'); 14 | const path = require('path'); 15 | const qs = require('qs'); 16 | const fetch = require('node-fetch'); 17 | const url = require('url'); 18 | const querystring = require('querystring'); 19 | const njwt = require('njwt'); 20 | const constants = require('./constants'); 21 | const OktaJwtVerifier = require('../lib'); 22 | 23 | const ISSUER = constants.ISSUER; 24 | const OKTA_TESTING_DISABLEHTTPSCHECK = constants.OKTA_TESTING_DISABLEHTTPSCHECK 25 | 26 | const NODE_MODULES = path.resolve(__dirname, '../node_modules'); 27 | const publicKeyPath = path.normalize(path.join(NODE_MODULES, '/njwt/test/rsa.pub')); 28 | const privateKeyPath = path.normalize(path.join(NODE_MODULES, '/njwt/test/rsa.priv')); 29 | const wrongPublicKeyPath = path.normalize(path.join(__dirname, '/keys/rsa-fake.pub')); 30 | const rsaKeyPair = { 31 | public: fs.readFileSync(publicKeyPath, 'utf8'), 32 | private: fs.readFileSync(privateKeyPath, 'utf8'), 33 | wrongPublic: fs.readFileSync(wrongPublicKeyPath, 'utf8') 34 | }; 35 | 36 | 37 | function getTokens(options = {}) { 38 | const { 39 | ISSUER, 40 | CLIENT_ID, 41 | REDIRECT_URI, 42 | USERNAME, 43 | PASSWORD, 44 | NONCE, 45 | RESPONSE_TYPE 46 | } = options; 47 | 48 | return new Promise((resolve, reject) => { 49 | const urlProperties = url.parse(ISSUER); 50 | const domain = urlProperties.protocol + '//' + urlProperties.host; 51 | const postUrl = domain + '/api/v1/authn'; 52 | 53 | fetch(postUrl, { 54 | method: 'POST', 55 | headers: { 'Content-Type': 'application/json' }, 56 | body: JSON.stringify({ 57 | username: USERNAME, 58 | password: PASSWORD 59 | }) 60 | }).then(resp => { 61 | if (!resp.ok) { 62 | throw new Error(`/api/v1/authn returned error: ${resp.status}`); 63 | } 64 | 65 | return resp.json(); 66 | }).then(body => { 67 | if (!body.sessionToken) { 68 | throw new Error(`Could not pass sessionToken from ${postUrl}`); 69 | } 70 | 71 | const authorizeParams = { 72 | sessionToken: body.sessionToken, 73 | response_type: RESPONSE_TYPE || 'id_token token', 74 | client_id: CLIENT_ID, 75 | redirect_uri: REDIRECT_URI, 76 | scope: 'openid', 77 | state: 'foo', 78 | nonce: NONCE || 'foo' 79 | } 80 | const authorizeUrl = ISSUER + '/v1/authorize?' + qs.stringify(authorizeParams); 81 | 82 | return fetch(authorizeUrl, { redirect: 'manual' }); 83 | }).then(resp => { 84 | if (resp.status >= 400) { 85 | throw new Error(`/api/v1/authorize error: ${resp.status}`); 86 | } 87 | 88 | const parsedUrl = url.parse(resp.headers.get('location'), true); 89 | const parsedParams = parsedUrl.hash ? querystring.parse(parsedUrl.hash.slice(1)) : parsedUrl.query; 90 | 91 | if (parsedParams.error) { 92 | throw new Error(`/api/v1/authorize error in query: ${parsedParams.error}`); 93 | } 94 | 95 | resolve({ 96 | accessToken: parsedParams.access_token, 97 | idToken: parsedParams.id_token, 98 | }); 99 | }).catch(err => { 100 | console.error(err.message || err); 101 | reject(err) 102 | }); 103 | }); 104 | } 105 | 106 | function getKeySet() { 107 | return fetch(ISSUER + '/v1/keys') 108 | .then(response => { 109 | if (!response.ok) { 110 | throw new Error('Failed to get keys'); 111 | } 112 | return response.json(); 113 | }); 114 | } 115 | 116 | function getAccessToken(options = {}) { 117 | return getTokens({...options, RESPONSE_TYPE: 'token'}).then(({accessToken: accessToken}) => { 118 | if (!accessToken){ 119 | throw new Error('Could not parse access token from URI'); 120 | } 121 | return accessToken; 122 | }); 123 | } 124 | 125 | function getIdToken(options = {}) { 126 | return getTokens({...options, RESPONSE_TYPE: 'id_token'}).then(({idToken: idToken}) => { 127 | if (!idToken){ 128 | throw new Error('Could not parse ID token from URI'); 129 | } 130 | return idToken; 131 | }); 132 | } 133 | 134 | function createToken(claims, headers = {}) { 135 | let token = njwt.create(claims, rsaKeyPair.private, 'RS256'); 136 | 137 | for (const [k, v] of Object.entries(headers)) { 138 | token = token.setHeader(k, v); 139 | } 140 | 141 | return token.compact(); 142 | } 143 | 144 | function createVerifier(options = {}) { 145 | return new OktaJwtVerifier({ 146 | issuer: ISSUER, 147 | testing: { 148 | disableHttpsCheck: OKTA_TESTING_DISABLEHTTPSCHECK 149 | }, 150 | ...options 151 | }); 152 | } 153 | 154 | function createCustomClaimsVerifier(customClaims, otherClaims) { 155 | return { 156 | verify: function(jwt, cb) { 157 | cb(null, { 158 | body: { 159 | ...otherClaims, 160 | ...customClaims 161 | }, 162 | toString: () => 'fake', 163 | isExpired: () => false, 164 | isNotBefore: () => false 165 | }) 166 | } 167 | }; 168 | } 169 | 170 | module.exports = { 171 | getAccessToken, 172 | getIdToken, 173 | getKeySet, 174 | createToken, 175 | createVerifier, 176 | createCustomClaimsVerifier, 177 | rsaKeyPair 178 | }; 179 | --------------------------------------------------------------------------------