├── .github ├── dependabot.yml └── workflows │ ├── check-linked-issues.yml │ ├── ci.yml │ ├── notify-release.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.js ├── package.json ├── src ├── cache.d.ts ├── error.d.ts ├── error.js ├── get-jwks.d.ts └── get-jwks.js ├── test ├── cache.spec.js ├── constants.js ├── fast-jwt-integration.spec.js ├── fastify-jwt-integrations.spec.js ├── getJwk.spec.js ├── getJwkDiscovery.spec.js ├── getJwksUri.spec.js ├── getPublicKey.spec.js ├── getPublicKeyDiscovery.spec.js ├── private.pem ├── public.pem └── types │ └── get-jwks.test-d.ts └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/check-linked-issues.yml: -------------------------------------------------------------------------------- 1 | name: Check Linked Issues 2 | 'on': 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - reopened 8 | - synchronize 9 | jobs: 10 | check_pull_requests: 11 | runs-on: ubuntu-latest 12 | name: Check linked issues 13 | steps: 14 | - uses: nearform-actions/github-action-check-linked-issues@v1 15 | with: 16 | github-token: ${{ secrets.GITHUB_TOKEN }} 17 | exclude-branches: release/**, dependabot/** 18 | permissions: 19 | issues: read 20 | pull-requests: write 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | build: 9 | uses: pkgjs/action/.github/workflows/node-test.yaml@v0 10 | with: 11 | strategy-fail-fast: true 12 | test-command: npm run lint && npm run test 13 | automerge: 14 | needs: build 15 | runs-on: ubuntu-latest 16 | permissions: 17 | pull-requests: write 18 | contents: write 19 | steps: 20 | - uses: fastify/github-action-merge-dependabot@v3 21 | -------------------------------------------------------------------------------- /.github/workflows/notify-release.yml: -------------------------------------------------------------------------------- 1 | name: notify-release 2 | 'on': 3 | workflow_dispatch: 4 | schedule: 5 | - cron: 30 8 * * * 6 | release: 7 | types: 8 | - published 9 | issues: 10 | types: 11 | - closed 12 | jobs: 13 | setup: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | contents: read 18 | steps: 19 | - uses: nearform-actions/github-action-notify-release@v1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | semver: 6 | description: The semver to use 7 | required: true 8 | default: patch 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | pull_request: 15 | types: [closed] 16 | 17 | jobs: 18 | release: 19 | permissions: 20 | contents: write 21 | issues: write 22 | pull-requests: write 23 | id-token: write 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: nearform-actions/optic-release-automation-action@v4 27 | with: 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | npm-token: >- 30 | ${{ secrets[format('NPM_TOKEN_{0}', github.actor)] || 31 | secrets.NPM_TOKEN }} 32 | optic-token: >- 33 | ${{ secrets[format('OPTIC_TOKEN_{0}', github.actor)] || 34 | secrets.OPTIC_TOKEN }} 35 | semver: ${{ github.event.inputs.semver }} 36 | provenance: true 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | node_modules/ 5 | .npm 6 | .eslintcache 7 | .nyc_output 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@nearform.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to get-jwks! 2 | 3 | Please take a second to read over this before opening an issue. Providing complete information upfront will help us address any issue (and ship new features!) faster. 4 | 5 | We greatly appreciate bug fixes, documentation improvements and new features, however when contributing a new major feature, it is a good idea to idea to first open an issue, to make sure the feature it fits with the goal of the project, so we don't waste your or our time. 6 | 7 | ## Bug Reports 8 | 9 | A perfect bug report would have the following: 10 | 11 | 1. Summary of the issue you are experiencing. 12 | 2. Details on what versions of node and XZY you are using (`node -v`). 13 | 3. A simple repeatable test case for us to run. Please try to run through it 2-3 times to ensure it is completely repeatable. 14 | 15 | We would like to avoid issues that require a follow up questions to identify the bug. These follow ups are difficult to do unless we have a repeatable test case. 16 | 17 | ## For Developers 18 | 19 | All contributions should fit the [standard](https://github.com/standard/standard) linter, and pass the tests. 20 | You can test this by running: 21 | 22 | ``` 23 | npm lint 24 | npm test 25 | ``` 26 | 27 | ## For Collaborators 28 | 29 | Make sure to get a `:thumbsup:`, `+1` or `LGTM` from another collaborator before merging a PR. If you aren't sure if a release should happen, open an issue. 30 | 31 | Release process: 32 | 33 | - `npm test` 34 | - `npm version ` 35 | - `git push && git push --tags` 36 | - `npm publish` 37 | 38 | --- 39 | 40 | 41 | 42 | ## Developer's Certificate of Origin 1.1 43 | 44 | By making a contribution to this project, I certify that: 45 | 46 | - (a) The contribution was created in whole or in part by me and I 47 | have the right to submit it under the open source license 48 | indicated in the file; or 49 | 50 | - (b) The contribution is based upon previous work that, to the best 51 | of my knowledge, is covered under an appropriate open source 52 | license and I have the right under that license to submit that 53 | work with modifications, whether created in whole or in part 54 | by me, under the same open source license (unless I am 55 | permitted to submit under a different license), as indicated 56 | in the file; or 57 | 58 | - (c) The contribution was provided directly to me by some other 59 | person who certified (a), (b) or (c) and I have not modified 60 | it. 61 | 62 | - (d) I understand and agree that this project and the contribution 63 | are public and that a record of the contribution (including all 64 | personal information I submit with it, including my sign-off) is 65 | maintained indefinitely and may be redistributed consistent with 66 | this project or the open source license(s) involved. 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 NearForm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # get-jwks 2 | 3 | [![ci](https://github.com/nearform/get-jwks/actions/workflows/ci.yml/badge.svg)](https://github.com/nearform/get-jwks/actions/workflows/ci.yml) 4 | 5 | Fetch utils for JWKS keys 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install get-jwks 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Options 16 | 17 | ```js 18 | const https = require('node:https') 19 | const buildGetJwks = require('get-jwks') 20 | 21 | const getJwks = buildGetJwks({ 22 | max: 100, 23 | ttl: 60 * 1000, 24 | timeout: 5000, 25 | issuersWhitelist: ['https://example.com'], 26 | checkIssuer: issuer => { 27 | return issuer === 'https://example.com' 28 | }, 29 | providerDiscovery: false, 30 | fetchOptions: { 31 | headers: { 32 | 'Content-Type': 'application/json' 33 | } 34 | }, 35 | }) 36 | ``` 37 | 38 | - `max`: Max items to hold in cache. Defaults to 100. 39 | - `ttl`: Milliseconds an item will remain in cache. Defaults to 60s. 40 | - `timeout`: Specifies how long it should wait to retrieve a JWK before it fails. The time is set in milliseconds. Defaults to 5s. 41 | - `issuersWhitelist`: Array of allowed issuers. By default all issuers are allowed. 42 | - `allowedDomains`: This has been **deprecated** and replaced with `issuersWhitelist`. 43 | - `checkIssuer`: Optional user defined function to validate a token's domain. 44 | - `providerDiscovery`: Indicates if the Provider Configuration Information is used to automatically get the jwks_uri from the [OpenID Provider Discovery Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). This endpoint is exposing the [Provider Metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). With this flag set to true the domain will be treated as the OpenID Issuer which is the iss property in the token. Defaults to false. Ignored if jwksPath is specified. 45 | - `jwksPath`: Specify a relative path to the jwks_uri. Example `/otherdir/jwks.json`. Takes precedence over providerDiscovery. Optional. 46 | - `fetchOptions`: The custom [RequestInit](https://github.com/nodejs/undici/blob/c29be3b62702642d2ab505502e740d3212ed4b25/types/fetch.d.ts#L121) used to instrument `fetch`. Optional. 47 | 48 | > `max` and `ttl` are provided to [lru-cache](https://www.npmjs.com/package/lru-cache). 49 | 50 | ### getJwk 51 | 52 | ```js 53 | const buildGetJwks = require('get-jwks') 54 | 55 | const getJwks = buildGetJwks() 56 | 57 | const jwk = await getJwks.getJwk({ 58 | domain: 'https://example.com/', 59 | alg: 'token_alg', 60 | kid: 'token_kid', 61 | }) 62 | ``` 63 | 64 | Calling the asynchronous function `getJwk` will fetch the [JSON Web Key](https://tools.ietf.org/html/rfc7517), and verify if any of the public keys matches the provided `alg` (if any) and `kid` values. It will cache the matching key so if called again it will not make another request to retrieve a JWKS. It will also use a cache to store stale values which is used in case of errors as a fallback mechanism. 65 | 66 | - `domain`: A string containing the domain (e.g. `https://www.example.com/`, with or without trailing slash) from which the library should fetch the JWKS. If providerDiscovery flag is set to false `get-jwks` will add the JWKS location (`.well-known/jwks.json`) to form the final url (ie: `https://www.example.com/.well-known/jwks.json`) otherwise the domain will be treated as tthe openid issuer and the retrival will be done via the Provider Discovery Endpoint. 67 | - `alg`: The alg header parameter is an optional parameter that represents the cryptographic algorithm used to secure the token. You will find it in your decoded JWT. 68 | - `kid`: The kid is a hint that indicates which key was used to secure the JSON web signature of the token. You will find it in your decoded JWT. 69 | 70 | ### getPublicKey 71 | 72 | ```js 73 | const buildGetJwks = require('get-jwks') 74 | 75 | const getJwks = buildGetJwks() 76 | 77 | const publicKey = await getJwks.getPublicKey({ 78 | domain: 'https://exampe.com/', 79 | alg: 'token_alg', 80 | kid: 'token_kid', 81 | }) 82 | ``` 83 | 84 | Calling the asynchronous function `getPublicKey` will run the `getJwk` function to retrieve a matching key, then convert it to a PEM public key. It requires the same arguments as `getJwk`. 85 | 86 | ## Integration Examples 87 | 88 | This library can be easily used with other JWT libraries. 89 | 90 | ### @fastify/jwt 91 | 92 | [@fastify/jwt](https://github.com/fastify/fastify-jwt) is a Json Web Token plugin for [Fastify](https://www.fastify.io/). 93 | 94 | The following example includes a scenario where you'd like to verify a JWT against a valid JWK on any request to your Fastify server. Any request with a valid JWT auth token in the header will return a successful response, otherwise will respond with an authentication error. 95 | 96 | ```js 97 | const Fastify = require('fastify') 98 | const fjwt = require('@fastify/jwt') 99 | const buildGetJwks = require('get-jwks') 100 | 101 | const fastify = Fastify() 102 | const getJwks = buildGetJwks() 103 | 104 | fastify.register(fjwt, { 105 | decode: { complete: true }, 106 | issuersWhitelist: ['https://example.com'], 107 | checkIssuer: issuer => { 108 | return issuer === 'https://example.com' 109 | }, 110 | secret: (request, token, callback) => { 111 | const { 112 | header: { kid, alg }, 113 | payload: { iss }, 114 | } = token 115 | getJwks 116 | .getPublicKey({ kid, domain: iss, alg }) 117 | .then(publicKey => callback(null, publicKey), callback) 118 | }, 119 | }) 120 | 121 | fastify.addHook('onRequest', async (request, reply) => { 122 | await request.jwtVerify() 123 | }) 124 | 125 | fastify.listen(3000) 126 | ``` 127 | 128 | ### fast-jwt 129 | 130 | [fast-jwt](https://github.com/nearform/fast-jwt) is a fast JSON Web Token implementation. 131 | 132 | The following example shows how to use JWKS in fast-jwt via get-jwks. 133 | 134 | ```js 135 | const { createVerifier } = require('fast-jwt') 136 | const buildGetJwks = require('get-jwks') 137 | 138 | // well known url of the token issuer 139 | // often encoded as the `iss` property of the token payload 140 | const domain = 'https://...' 141 | 142 | const getJwks = buildGetJwks({ issuersWhitelist: [...]}) 143 | 144 | // create a verifier function with key as a function 145 | const verifyWithPromise = createVerifier({ 146 | key: async function ({ header }) { 147 | const publicKey = await getJwks.getPublicKey({ 148 | kid: header.kid, 149 | alg: header.alg, 150 | domain, 151 | }) 152 | return publicKey 153 | }, 154 | }) 155 | 156 | const payload = await verifyWithPromise(token) 157 | ``` 158 | 159 | [![banner](https://raw.githubusercontent.com/nearform/.github/refs/heads/master/assets/os-banner-green.svg)](https://www.nearform.com/contact/?utm_source=open-source&utm_medium=banner&utm_campaign=os-project-pages) 160 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const js = require('@eslint/js') 2 | const globals = require('globals') 3 | 4 | module.exports = [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | globals: { 9 | ...globals.node 10 | }, 11 | ecmaVersion: 2020, 12 | sourceType: "commonjs", 13 | } 14 | } 15 | ] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-jwks", 3 | "version": "11.0.1", 4 | "description": "Fetch utils for JWKS keys", 5 | "main": "src/get-jwks.js", 6 | "types": "./src/get-jwks.d.ts", 7 | "files": [ 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=20" 12 | }, 13 | "tsd": { 14 | "directory": "./test/types" 15 | }, 16 | "scripts": { 17 | "test": "node --test test/*.spec.js && tsd", 18 | "lint": "eslint ." 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/nearform/get-jwks.git" 23 | }, 24 | "keywords": [ 25 | "jwks", 26 | "jwt" 27 | ], 28 | "author": "Filippo De Santis ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/nearform/get-jwks/issues" 32 | }, 33 | "homepage": "https://github.com/nearform/get-jwks#readme", 34 | "dependencies": { 35 | "jwk-to-pem": "^2.0.7", 36 | "lru-cache": "^11.1.0" 37 | }, 38 | "devDependencies": { 39 | "@fastify/jwt": "^9.1.0", 40 | "@types/node": "^22.14.0", 41 | "eslint": "^9.24.0", 42 | "fast-jwt": "^6.0.1", 43 | "fastify": "^5.2.2", 44 | "jsonwebtoken": "^9.0.2", 45 | "nock": "^v14.0.5", 46 | "prettier": "^3.5.3", 47 | "sinon": "^20.0.0", 48 | "tsd": "^0.32.0", 49 | "typescript": "^5.8.3", 50 | "undici": "~7.10.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/cache.d.ts: -------------------------------------------------------------------------------- 1 | interface Cache { 2 | get(key: K): V | undefined; 3 | set(key: K, value: V, ttl?: number): void; 4 | delete(key: K): void; 5 | } 6 | 7 | export = Cache 8 | -------------------------------------------------------------------------------- /src/error.d.ts: -------------------------------------------------------------------------------- 1 | export enum errorCode { 2 | OPENID_CONFIGURATION_REQUEST_FAILED = 'OPENID_CONFIGURATION_REQUEST_FAILED', 3 | JWKS_REQUEST_FAILED = 'JWKS_REQUEST_FAILED', 4 | NO_JWKS_URI = 'NO_JWKS_URI', 5 | NO_JWKS = 'NO_JWKS', 6 | JWK_NOT_FOUND = 'JWK_NOT_FOUND', 7 | DOMAIN_NOT_ALLOWED = 'DOMAIN_NOT_ALLOWED', 8 | } 9 | 10 | declare class GetJwksError extends Error { 11 | code: errorCode 12 | response: Response 13 | body: unknown 14 | 15 | constructor( 16 | code: errorCode, 17 | requestProperties?: { response: Response; body: unknown } 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const errorCode = { 4 | OPENID_CONFIGURATION_REQUEST_FAILED: 'OPENID_CONFIGURATION_REQUEST_FAILED', 5 | JWKS_REQUEST_FAILED: 'JWKS_REQUEST_FAILED', 6 | NO_JWKS_URI: 'NO_JWKS_URI', 7 | NO_JWKS: 'NO_JWKS', 8 | JWK_NOT_FOUND: 'JWK_NOT_FOUND', 9 | DOMAIN_NOT_ALLOWED: 'DOMAIN_NOT_ALLOWED', 10 | } 11 | 12 | const errors = { 13 | [errorCode.OPENID_CONFIGURATION_REQUEST_FAILED]: 14 | 'OpenID configuration request failed', 15 | [errorCode.JWKS_REQUEST_FAILED]: 'JWKS request failed', 16 | [errorCode.NO_JWKS_URI]: 'No valid jwks_uri key found in providerConfig', 17 | [errorCode.NO_JWKS]: 'No JWKS found in the response.', 18 | [errorCode.JWK_NOT_FOUND]: 'No matching JWK found in the set.', 19 | [errorCode.DOMAIN_NOT_ALLOWED]: 'The domain is not allowed.', 20 | } 21 | 22 | class GetJwksError extends Error { 23 | constructor(code, requestProperties = {}) { 24 | super(errors[code]) 25 | 26 | this.name = GetJwksError.name 27 | this.code = code 28 | 29 | this.response = requestProperties.response 30 | this.body = requestProperties.body 31 | } 32 | } 33 | 34 | module.exports = { 35 | errorCode, 36 | GetJwksError, 37 | } 38 | -------------------------------------------------------------------------------- /src/get-jwks.d.ts: -------------------------------------------------------------------------------- 1 | import Cache from './cache'; 2 | 3 | type GetPublicKeyOptions = { 4 | domain?: string 5 | alg?: string 6 | kid?: string 7 | } 8 | 9 | type JWKSignature = { domain: string; alg: string; kid: string } 10 | type JWK = { [key: string]: any; domain: string; alg: string; kid: string } 11 | 12 | type GetJwks = { 13 | getPublicKey: (options?: GetPublicKeyOptions) => Promise 14 | getJwk: (signature: JWKSignature) => Promise 15 | getJwksUri: (normalizedDomain: string) => Promise 16 | cache: Cache 17 | staleCache: Cache 18 | } 19 | 20 | type GetJwksOptions = { 21 | max?: number 22 | ttl?: number 23 | issuersWhitelist?: string[] 24 | providerDiscovery?: boolean 25 | jwksPath?: string 26 | fetchOptions?: import("undici").RequestInit 27 | } 28 | 29 | declare namespace buildGetJwks { 30 | export type { JWKSignature, JWK, GetPublicKeyOptions, GetJwksOptions, GetJwks } 31 | } 32 | 33 | declare function buildGetJwks(options?: GetJwksOptions): GetJwks 34 | export = buildGetJwks -------------------------------------------------------------------------------- /src/get-jwks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { LRUCache } = require('lru-cache') 4 | const jwkToPem = require('jwk-to-pem') 5 | 6 | const { errorCode, GetJwksError } = require('./error') 7 | 8 | const ONE_MINUTE = 60 * 1000 9 | const FIVE_SECONDS = 5 * 1000 10 | 11 | function ensureTrailingSlash(domain) { 12 | return domain[domain.length - 1] === '/' ? domain : `${domain}/` 13 | } 14 | 15 | function ensureNoLeadingSlash(path) { 16 | return path[0] === '/' ? path.substring(1) : path 17 | } 18 | 19 | function buildGetJwks(options = {}) { 20 | const max = options.max || 100 21 | const ttl = options.ttl || ONE_MINUTE 22 | const issuersWhitelist = (options.issuersWhitelist || []).map(ensureTrailingSlash) 23 | const checkIssuer = options.checkIssuer 24 | const providerDiscovery = options.providerDiscovery || false 25 | const jwksPath = options.jwksPath 26 | ? ensureNoLeadingSlash(options.jwksPath) 27 | : false 28 | const fetchOptions = { timeout: FIVE_SECONDS, ...options.fetchOptions } 29 | const staleCache = new LRUCache({ max: max * 2, ttl }) 30 | const cache = new LRUCache({ 31 | max, 32 | ttl, 33 | dispose: (value, key) => staleCache.set(key, value), 34 | }) 35 | 36 | async function getJwksUri(normalizedDomain) { 37 | const response = await fetch( 38 | `${normalizedDomain}.well-known/openid-configuration`, 39 | fetchOptions, 40 | ) 41 | const body = await response.json() 42 | 43 | if (!response.ok) { 44 | throw new GetJwksError(errorCode.OPENID_CONFIGURATION_REQUEST_FAILED, { 45 | response, 46 | body, 47 | }) 48 | } 49 | 50 | if (!body.jwks_uri) { 51 | throw new GetJwksError(errorCode.NO_JWKS_URI) 52 | } 53 | 54 | return body.jwks_uri 55 | } 56 | 57 | async function getPublicKey(signature) { 58 | return jwkToPem(await this.getJwk(signature)) 59 | } 60 | 61 | function getJwk(signature) { 62 | const { domain, alg, kid } = signature 63 | 64 | const normalizedDomain = ensureTrailingSlash(domain) 65 | 66 | if (issuersWhitelist.length && !issuersWhitelist.includes(normalizedDomain)) { 67 | const error = new GetJwksError(errorCode.DOMAIN_NOT_ALLOWED) 68 | return Promise.reject(error) 69 | } 70 | 71 | if (checkIssuer && !checkIssuer(normalizedDomain)) { 72 | const error = new GetJwksError(errorCode.DOMAIN_NOT_ALLOWED) 73 | return Promise.reject(error) 74 | } 75 | 76 | const cacheKey = `${alg}:${kid}:${normalizedDomain}` 77 | const cachedJwk = cache.get(cacheKey) 78 | 79 | if (cachedJwk) { 80 | return cachedJwk 81 | } 82 | 83 | const jwkPromise = retrieveJwk(normalizedDomain, alg, kid).catch( 84 | err => { 85 | const stale = staleCache.get(cacheKey) 86 | 87 | cache.delete(cacheKey) 88 | 89 | if (stale) { 90 | return stale 91 | } 92 | 93 | throw err 94 | } 95 | ) 96 | 97 | cache.set(cacheKey, jwkPromise) 98 | 99 | return jwkPromise 100 | } 101 | 102 | async function retrieveJwk(normalizedDomain, alg, kid) { 103 | const jwksUri = jwksPath 104 | ? normalizedDomain + jwksPath 105 | : providerDiscovery 106 | ? await getJwksUri(normalizedDomain) 107 | : `${normalizedDomain}.well-known/jwks.json` 108 | 109 | const response = await fetch(jwksUri, fetchOptions) 110 | const body = await response.json() 111 | 112 | if (!response.ok) { 113 | throw new GetJwksError(errorCode.JWKS_REQUEST_FAILED, { 114 | response, 115 | body, 116 | }) 117 | } 118 | 119 | if (!body.keys?.length) { 120 | throw new GetJwksError(errorCode.NO_JWKS) 121 | } 122 | 123 | const jwk = body.keys.find( 124 | key => 125 | (alg === undefined || key.alg === undefined || key.alg === alg) && 126 | key.kid === kid 127 | ) 128 | 129 | if (!jwk) { 130 | throw new GetJwksError(errorCode.JWK_NOT_FOUND) 131 | } 132 | 133 | return jwk 134 | } 135 | 136 | return { 137 | getPublicKey, 138 | getJwk, 139 | getJwksUri, 140 | cache, 141 | staleCache, 142 | } 143 | } 144 | 145 | module.exports = buildGetJwks 146 | -------------------------------------------------------------------------------- /test/cache.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test} = require('node:test') 4 | const nock = require('nock') 5 | const jwkToPem = require('jwk-to-pem') 6 | 7 | const { jwks, domain } = require('./constants') 8 | 9 | const buildGetJwks = require('../src/get-jwks') 10 | 11 | test( 12 | 'if there is already a key in cache, it should not make a http request', 13 | async t => { 14 | const getJwks = buildGetJwks() 15 | const localKey = jwks.keys[0] 16 | const alg = localKey.alg 17 | const kid = localKey.kid 18 | 19 | getJwks.cache.set(`${alg}:${kid}:${domain}`, Promise.resolve(localKey)) 20 | 21 | const publicKey = await getJwks.getPublicKey({ domain, alg, kid }) 22 | const jwk = await getJwks.getJwk({ domain, alg, kid }) 23 | t.assert.ok(publicKey) 24 | t.assert.ok(jwk) 25 | t.assert.equal(publicKey, jwkToPem(jwk)) 26 | t.assert.equal(jwk, localKey) 27 | } 28 | ) 29 | 30 | test( 31 | 'if initialized without any cache settings it should use default values', 32 | async t => { 33 | nock('https://localhost/').get('/.well-known/jwks.json').reply(200, jwks) 34 | const getJwks = buildGetJwks() 35 | const cache = getJwks.cache 36 | const [{ alg, kid }] = jwks.keys 37 | const publicKey = await getJwks.getPublicKey({ domain, alg, kid }) 38 | const jwk = await getJwks.getJwk({ domain, alg, kid }) 39 | 40 | t.assert.ok(publicKey) 41 | t.assert.ok(jwk) 42 | t.assert.ok(getJwks.cache) 43 | t.assert.equal(cache.max, 100) 44 | t.assert.equal(cache.ttl, 60000) 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { readFileSync } = require('node:fs') 4 | const path = require('node:path') 5 | const jwt = require('jsonwebtoken') 6 | 7 | const domain = 'https://localhost/' 8 | 9 | const jwks = { 10 | keys: [ 11 | { 12 | alg: 'RS512', 13 | kid: 'KEY_0', 14 | e: 'AQAB', 15 | kty: 'RSA', 16 | n: 17 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImZmMTBmMTg1LWFiODEtNDhjYS1hZmI1LTdkY2FhMzNmYzgzNSIsImlhdCI6MTYxNDEwMzkxNiwiZXhwIjoxNjE0MTA3NTE2fQ.mLx1TZaHDhcymZFmLM7pfBhowY7CEgjuxr54LPXpGXc', 18 | use: 'sig', 19 | }, 20 | { 21 | alg: 'RS256', 22 | kid: 'KEY_1', 23 | e: 'AQAB', 24 | kty: 'RSA', 25 | n: 26 | '7KRDtHuJ9-R1cYzB9-E4TUVazzv93MMmMo_38nOwEKNxlWs7OVg397d0SCsdmBbcbr4KTMeblY4a-VOzLVZ5ycYgi7ZbMvv7RzunKuPsjm7m863dLnPUFOptsFVANDOHgDYopKBFYoIMoxjXU7bOzLL-Ez0oO5keT1hGZkJT_7GRvKyYigugN4lLia4Tb3AmUN60wiloyQCJ2xYATWHB0e4sTwIDq6MFXhVFHXV6ZBU7sDh0HqmP08gJtMnsFOE7zUcbpqTvpz5nAR6EyUs7R0g61WmGUfQTrE6byVCZ8w0NN4Xer6IQBjnDZWbmf69jsAFFAYDCe-omWXY526qLQw', 27 | use: 'sig', 28 | }, 29 | { 30 | kid: 'KEY_2', 31 | e: 'AQAB', 32 | kty: 'RSA', 33 | n: 34 | '7KRDtHuJ9-R1cYzB9-E4TUVazzv93MMmMo_38nOwEKNxlWs7OVg397d0SCsdmBbcbr4KTMeblY4a-VOzLVZ5ycYgi7ZbMvv7RzunKuPsjm7m863dLnPUFOptsFVANDOHgDYopKBFYoIMoxjXU7bOzLL-Ez0oO5keT1hGZkJT_7GRvKyYigugN4lLia4Tb3AmUN60wiloyQCJ2xYATWHB0e4sTwIDq6MFXhVFHXV6ZBU7sDh0HqmP08gJtMnsFOE7zUcbpqTvpz5nAR6EyUs7R0g61WmGUfQTrE6byVCZ8w0NN4Xer6IQBjnDZWbmf69jsAFFAYDCe-omWXY526qLQw', 35 | use: 'sig' 36 | } 37 | ], 38 | } 39 | 40 | const privateKey = readFileSync(path.join(__dirname, 'private.pem'), 'utf8') 41 | 42 | const jwk = jwks.keys[1] 43 | 44 | const token = jwt.sign({ name: 'Jane Doe' }, privateKey, { 45 | algorithm: jwk.alg, 46 | issuer: domain, 47 | keyid: jwk.kid, 48 | }) 49 | 50 | const oidcConfig = { 51 | issuer: 'https://localhost/', 52 | jwks_uri: 'https://localhost/.well-known/certs', 53 | } 54 | 55 | module.exports = { 56 | oidcConfig, 57 | domain, 58 | token, 59 | jwks, 60 | } 61 | -------------------------------------------------------------------------------- /test/fast-jwt-integration.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {beforeEach, afterEach, test} = require('node:test') 4 | const nock = require('nock') 5 | const { createVerifier } = require('fast-jwt') 6 | 7 | const { jwks, token } = require('./constants') 8 | const buildGetJwks = require('../src/get-jwks') 9 | 10 | beforeEach(() => { 11 | nock.disableNetConnect() 12 | }) 13 | 14 | afterEach(() => { 15 | nock.cleanAll() 16 | nock.enableNetConnect() 17 | }) 18 | 19 | test('fast-jwt integration tests', async t => { 20 | const domain = 'https://localhost/' 21 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 22 | 23 | const getJwks = buildGetJwks() 24 | const verifyWithPromise = createVerifier({ 25 | key: async function ({ header }) { 26 | const publicKey = await getJwks.getPublicKey({ 27 | kid: header.kid, 28 | alg: header.alg, 29 | domain, 30 | }) 31 | return publicKey 32 | }, 33 | }) 34 | const payload = await verifyWithPromise(token) 35 | 36 | t.assert.equal(payload.name, 'Jane Doe') 37 | }) 38 | -------------------------------------------------------------------------------- /test/fastify-jwt-integrations.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { beforeEach, afterEach, test } = require('node:test') 4 | const nock = require('nock') 5 | const Fastify = require('fastify') 6 | const fjwt = require('@fastify/jwt') 7 | 8 | const { oidcConfig, jwks, token, domain } = require('./constants') 9 | const buildGetJwks = require('../src/get-jwks') 10 | 11 | beforeEach(() => { 12 | nock.disableNetConnect() 13 | }) 14 | 15 | afterEach(() => { 16 | nock.cleanAll() 17 | nock.enableNetConnect() 18 | }) 19 | 20 | test('@fastify/jwt integration tests', async t => { 21 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 22 | 23 | const fastify = Fastify() 24 | const getJwks = buildGetJwks() 25 | 26 | fastify.register(fjwt, { 27 | decode: { complete: true }, 28 | secret: (request, token, callback) => { 29 | const { 30 | header: { kid, alg }, 31 | payload: { iss }, 32 | } = token 33 | getJwks 34 | .getPublicKey({ kid, domain: iss, alg }) 35 | .then(publicKey => callback(null, publicKey), callback) 36 | }, 37 | }) 38 | 39 | fastify.addHook('onRequest', async request => { 40 | await request.jwtVerify() 41 | }) 42 | 43 | fastify.get('/', async request => { 44 | return request.user.name 45 | }) 46 | 47 | const response = await fastify.inject({ 48 | method: 'GET', 49 | url: '/', 50 | headers: { 51 | authorization: `Bearer ${token}`, 52 | }, 53 | }) 54 | 55 | t.assert.equal(response.statusCode, 200) 56 | t.assert.equal(response.body, 'Jane Doe') 57 | }) 58 | 59 | test('@fastify/jwt integration tests with providerDiscovery', async t => { 60 | nock(domain) 61 | .get('/.well-known/openid-configuration') 62 | .once() 63 | .reply(200, oidcConfig) 64 | nock(domain).get('/.well-known/certs').reply(200, jwks) 65 | 66 | const fastify = Fastify() 67 | const getJwks = buildGetJwks({ providerDiscovery: true }) 68 | 69 | fastify.register(fjwt, { 70 | decode: { complete: true }, 71 | secret: (request, token, callback) => { 72 | const { 73 | header: { kid, alg }, 74 | payload: { iss }, 75 | } = token 76 | getJwks 77 | .getPublicKey({ kid, domain: iss, alg }) 78 | .then(publicKey => callback(null, publicKey), callback) 79 | }, 80 | }) 81 | fastify.addHook('onRequest', async request => { 82 | await request.jwtVerify() 83 | }) 84 | 85 | fastify.get('/', async request => { 86 | return request.user.name 87 | }) 88 | 89 | const response = await fastify.inject({ 90 | method: 'GET', 91 | url: '/', 92 | headers: { 93 | authorization: `Bearer ${token}`, 94 | }, 95 | }) 96 | 97 | t.assert.equal(response.statusCode, 200) 98 | t.assert.equal(response.body, 'Jane Doe') 99 | }) 100 | -------------------------------------------------------------------------------- /test/getJwk.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nock = require('nock') 4 | const {beforeEach, afterEach, test, describe} = require('node:test') 5 | 6 | const { jwks, domain } = require('./constants') 7 | const buildGetJwks = require('../src/get-jwks') 8 | const { GetJwksError, errorCode } = require('../src/error') 9 | 10 | beforeEach(() => { 11 | nock.disableNetConnect() 12 | }) 13 | 14 | afterEach(() => { 15 | nock.cleanAll() 16 | nock.enableNetConnect() 17 | }) 18 | 19 | test('rejects if the request fails', async t => { 20 | nock(domain).get('/.well-known/jwks.json').reply(500, { msg: 'boom' }) 21 | 22 | const [{ alg, kid }] = jwks.keys 23 | const getJwks = buildGetJwks() 24 | 25 | const expectedError = { 26 | name: GetJwksError.name, 27 | code: errorCode.JWKS_REQUEST_FAILED, 28 | body: { msg: 'boom' }, 29 | } 30 | 31 | await t.assert.rejects(getJwks.getJwk({ domain, alg, kid }), expectedError) 32 | }) 33 | 34 | test('rejects if alg and kid do not match', async t => { 35 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 36 | 37 | const getJwks = buildGetJwks() 38 | 39 | await t.assert.rejects( 40 | getJwks.getJwk({ domain, alg: 'NOT', kid: 'FOUND' }), 41 | new GetJwksError('JWK_NOT_FOUND', 'No matching JWK found in the set.'), 42 | ) 43 | }) 44 | 45 | test('returns a jwk if alg and kid match', async t => { 46 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 47 | const getJwks = buildGetJwks() 48 | const key = jwks.keys[0] 49 | 50 | const jwk = await getJwks.getJwk({ domain, alg: key.alg, kid: key.kid }) 51 | 52 | t.assert.ok(jwk) 53 | t.assert.deepStrictEqual(jwk, key) 54 | }) 55 | 56 | test('returns a jwk if alg and kid match and path is specified', async t => { 57 | nock(domain).get('/otherdir/jwks.json').reply(200, jwks) 58 | const getJwks = buildGetJwks({ jwksPath: '/otherdir/jwks.json' }) 59 | const key = jwks.keys[0] 60 | 61 | const jwk = await getJwks.getJwk({ 62 | domain, 63 | alg: key.alg, 64 | kid: key.kid, 65 | }) 66 | 67 | t.assert.ok(jwk) 68 | t.assert.deepStrictEqual(jwk, key) 69 | }) 70 | 71 | test('returns a jwk if no alg is provided and kid match', async t => { 72 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 73 | const getJwks = buildGetJwks() 74 | const key = jwks.keys[2] 75 | 76 | const jwk = await getJwks.getJwk({ domain, kid: key.kid }) 77 | 78 | t.assert.ok(jwk) 79 | t.assert.deepStrictEqual(jwk, key) 80 | }) 81 | 82 | test( 83 | 'returns a jwk if no alg is provided and kid match but jwk has alg', 84 | async t => { 85 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 86 | const getJwks = buildGetJwks() 87 | const key = jwks.keys[1] 88 | 89 | const jwk = await getJwks.getJwk({ domain, kid: key.kid }) 90 | 91 | t.assert.ok(jwk) 92 | t.assert.deepStrictEqual(jwk, key) 93 | }, 94 | ) 95 | 96 | test('caches a successful response', async t => { 97 | nock(domain).get('/.well-known/jwks.json').once().reply(200, jwks) 98 | 99 | const getJwks = buildGetJwks() 100 | const key = jwks.keys[0] 101 | const { alg, kid } = key 102 | 103 | await getJwks.getJwk({ domain, alg, kid }) 104 | const jwk = await getJwks.getJwk({ domain, alg, kid }) 105 | 106 | t.assert.ok(jwk) 107 | t.assert.deepStrictEqual(jwk, key) 108 | }) 109 | 110 | test('does not cache a failed response', async t => { 111 | nock(domain).get('/.well-known/jwks.json').once().reply(500, { msg: 'boom' }) 112 | nock(domain).get('/.well-known/jwks.json').once().reply(200, jwks) 113 | 114 | const [{ alg, kid }] = jwks.keys 115 | const getJwks = buildGetJwks() 116 | 117 | await t.assert.rejects(getJwks.getJwk({ domain, alg, kid })) 118 | await getJwks.getJwk({ domain, alg, kid }) 119 | }) 120 | 121 | test('rejects if response is an empty object', async t => { 122 | nock(domain).get('/.well-known/jwks.json').reply(200, {}) 123 | const getJwks = buildGetJwks() 124 | const [{ alg, kid }] = jwks.keys 125 | 126 | return t.assert.rejects( 127 | getJwks.getJwk({ domain, alg, kid }), 128 | new GetJwksError('NO_JWKS', 'No JWKS found in the response.'), 129 | ) 130 | }) 131 | 132 | test('rejects if no JWKS are found in the response', async t => { 133 | nock(domain).get('/.well-known/jwks.json').reply(200, { keys: [] }) 134 | const getJwks = buildGetJwks() 135 | const [{ alg, kid }] = jwks.keys 136 | 137 | return t.assert.rejects( 138 | getJwks.getJwk({ domain, alg, kid }), 139 | new GetJwksError('NO_JWKS', 'No JWKS found in the response.'), 140 | ) 141 | }) 142 | 143 | test('supports domain without trailing slash', async t => { 144 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 145 | const getJwks = buildGetJwks() 146 | const [{ alg, kid }] = jwks.keys 147 | 148 | const key = await getJwks.getJwk({ domain: 'https://localhost', alg, kid }) 149 | t.assert.ok(key) 150 | }) 151 | 152 | test('supports path without leading slash', async t => { 153 | nock(domain).get('/otherdir/jwks.json').reply(200, jwks) 154 | const getJwks = buildGetJwks({ jwksPath: 'otherdir/jwks.json' }) 155 | const [{ alg, kid }] = jwks.keys 156 | 157 | const key = await getJwks.getJwk({ domain: 'https://localhost', alg, kid }) 158 | t.assert.ok(key) 159 | }) 160 | 161 | test('does not execute concurrent requests', () => { 162 | nock(domain).get('/.well-known/jwks.json').once().reply(200, jwks) 163 | 164 | const getJwks = buildGetJwks() 165 | const [{ alg, kid }] = jwks.keys 166 | 167 | return Promise.all([ 168 | getJwks.getJwk({ domain, alg, kid }), 169 | getJwks.getJwk({ domain, alg, kid }), 170 | ]) 171 | }) 172 | 173 | test('returns a stale cached value if request fails', async t => { 174 | // allow 2 requests, third will throw an error 175 | nock(domain).get('/.well-known/jwks.json').twice().reply(200, jwks) 176 | nock(domain).get('/.well-known/jwks.json').once().reply(500, { boom: true }) 177 | 178 | // allow only 1 entry in cache 179 | const getJwks = buildGetJwks({ max: 1 }) 180 | const [key1, key2] = jwks.keys 181 | 182 | // request key1 183 | await getJwks.getJwk({ 184 | domain: 'https://localhost', 185 | alg: key1.alg, 186 | kid: key1.kid, 187 | }) 188 | 189 | // request key2 190 | await getJwks.getJwk({ 191 | domain: 'https://localhost', 192 | alg: key2.alg, 193 | kid: key2.kid, 194 | }) 195 | 196 | // now key1 is stale 197 | // request key1 198 | const key = await getJwks.getJwk({ 199 | domain: 'https://localhost', 200 | alg: key1.alg, 201 | kid: key1.kid, 202 | }) 203 | 204 | t.assert.deepStrictEqual(key, key1) 205 | }) 206 | 207 | describe('allowed domains', () => { 208 | test('allows any domain by default', async t => { 209 | const domain = 'https://example.com' 210 | 211 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 212 | 213 | const getJwks = buildGetJwks() 214 | 215 | const [{ alg, kid }] = jwks.keys 216 | 217 | t.assert.ok(await getJwks.getJwk({ domain, alg, kid })) 218 | }) 219 | 220 | const allowedCombinations = [ 221 | // same without trailing slash 222 | ['https://example.com', 'https://example.com'], 223 | // same with trailing slash 224 | ['https://example.com/', 'https://example.com/'], 225 | // one without, one with 226 | ['https://example.com', 'https://example.com/'], 227 | // one with, one without 228 | ['https://example.com/', 'https://example.com'], 229 | ] 230 | 231 | allowedCombinations.forEach(([allowedDomain, domainFromToken]) => { 232 | test( 233 | `allows domain ${allowedDomain} requested with ${domainFromToken}`, 234 | async t => { 235 | nock(allowedDomain).get('/.well-known/jwks.json').reply(200, jwks) 236 | 237 | const getJwks = buildGetJwks({ 238 | issuersWhitelist: [allowedDomain], 239 | }) 240 | 241 | const [{ alg, kid }] = jwks.keys 242 | 243 | t.assert.ok(await getJwks.getJwk({ domain: domainFromToken, alg, kid })) 244 | }, 245 | ) 246 | }) 247 | 248 | test('allows multiple domains', async t => { 249 | const domain1 = 'https://example1.com' 250 | const domain2 = 'https://example2.com' 251 | 252 | nock(domain1).get('/.well-known/jwks.json').reply(200, jwks) 253 | nock(domain2).get('/.well-known/jwks.json').reply(200, jwks) 254 | 255 | const getJwks = buildGetJwks({ issuersWhitelist: [domain1, domain2] }) 256 | 257 | const [{ alg, kid }] = jwks.keys 258 | 259 | t.assert.ok(await getJwks.getJwk({ domain: domain1, alg, kid })) 260 | t.assert.ok(await getJwks.getJwk({ domain: domain2, alg, kid })) 261 | }) 262 | 263 | test('checks token issuer', async t => { 264 | const domain = 'https://example.com/realms/REALM_NAME' 265 | 266 | nock(domain).get('/.well-known/jwks.json').reply(200, jwks) 267 | 268 | const getJwks = buildGetJwks({ 269 | checkIssuer: (issuer) => { 270 | const url = new URL(issuer) 271 | const baseUrl = `${url.protocol}//${url.hostname}/` 272 | return baseUrl === 'https://example.com/' 273 | } 274 | }) 275 | 276 | const [{ alg, kid }] = jwks.keys 277 | 278 | t.assert.ok(await getJwks.getJwk({ domain, alg, kid })) 279 | }) 280 | 281 | test('forbids invalid issuer', async t => { 282 | const getJwks = buildGetJwks({ 283 | checkIssuer: (issuer) => { 284 | const url = new URL(issuer) 285 | const baseUrl = `${url.protocol}//${url.hostname}/` 286 | return baseUrl === 'https://example.com/' 287 | } 288 | }) 289 | 290 | const [{ alg, kid }] = jwks.keys 291 | 292 | return t.assert.rejects( 293 | getJwks.getJwk({ domain, alg, kid }), 294 | 'Issuer is not allowed.', 295 | ) 296 | }) 297 | 298 | test('forbids domain outside of the allow list', async t => { 299 | const getJwks = buildGetJwks({ 300 | issuersWhitelist: ['https://example.com/'], 301 | }) 302 | 303 | const [{ alg, kid }] = jwks.keys 304 | 305 | return t.assert.rejects( 306 | getJwks.getJwk({ domain, alg, kid }), 307 | new GetJwksError('DOMAIN_NOT_ALLOWED', 'The domain is not allowed.'), 308 | ) 309 | }) 310 | }) 311 | 312 | describe('timeout', () => { 313 | const domain = 'https://example.com' 314 | const [{ alg, kid }] = jwks.keys 315 | 316 | let timeout 317 | 318 | beforeEach(() => { 319 | global.fetch = (url, options) => { 320 | timeout = options.timeout 321 | return Promise.resolve({ 322 | ok: true, 323 | json: () => Promise.resolve(jwks) 324 | }) 325 | } 326 | }) 327 | 328 | test('timeout defaults to 5 seconds', async t => { 329 | const getJwks = buildGetJwks() 330 | await getJwks.getJwk({ domain, alg, kid }) 331 | t.assert.equal(timeout, 5000) 332 | }) 333 | 334 | test('ensures that timeout is set to 10 seconds', async t => { 335 | const getJwks = buildGetJwks({ fetchOptions: { timeout: 10000 } }) 336 | await getJwks.getJwk({ domain, alg, kid }) 337 | t.assert.equal(timeout, 10000) 338 | }) 339 | }) 340 | -------------------------------------------------------------------------------- /test/getJwkDiscovery.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const nock = require('nock') 4 | const {beforeEach, afterEach, test, describe} = require('node:test') 5 | 6 | const { oidcConfig, jwks, domain } = require('./constants') 7 | const buildGetJwks = require('../src/get-jwks') 8 | const { errorCode, GetJwksError } = require('../src/error') 9 | 10 | beforeEach(() => { 11 | nock.disableNetConnect() 12 | }) 13 | 14 | afterEach(() => { 15 | nock.cleanAll() 16 | nock.enableNetConnect() 17 | }) 18 | 19 | test('rejects if the discovery request fails', async t => { 20 | nock(domain) 21 | .get('/.well-known/openid-configuration') 22 | .reply(500, { msg: 'baam' }) 23 | 24 | const [{ alg, kid }] = jwks.keys 25 | const getJwks = buildGetJwks({ providerDiscovery: true }) 26 | 27 | const expectedError = { 28 | name: GetJwksError.name, 29 | code: errorCode.OPENID_CONFIGURATION_REQUEST_FAILED, 30 | body: { msg: 'baam' }, 31 | } 32 | await t.assert.rejects(getJwks.getJwk({ domain, alg, kid }), expectedError) 33 | }) 34 | 35 | test('rejects if the request fails', async t => { 36 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 37 | nock(domain).get('/.well-known/certs').reply(500, { msg: 'boom' }) 38 | 39 | const [{ alg, kid }] = jwks.keys 40 | const getJwks = buildGetJwks({ providerDiscovery: true }) 41 | 42 | const expectedError = { 43 | name: GetJwksError.name, 44 | code: errorCode.JWKS_REQUEST_FAILED, 45 | body: { msg: 'boom' }, 46 | } 47 | expectedError.body = { msg: 'boom' } 48 | 49 | await t.assert.rejects(getJwks.getJwk({ domain, alg, kid }), expectedError) 50 | }) 51 | 52 | test('returns a jwk if alg and kid match for discovery', async t => { 53 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 54 | nock(domain).get('/.well-known/certs').reply(200, jwks) 55 | const getJwks = buildGetJwks({ providerDiscovery: true }) 56 | const key = jwks.keys[0] 57 | 58 | const jwk = await getJwks.getJwk({ domain, alg: key.alg, kid: key.kid }) 59 | 60 | t.assert.ok(jwk) 61 | t.assert.deepStrictEqual(jwk, key) 62 | }) 63 | 64 | test( 65 | 'returns a jwk if no alg is provided and kid match for discovery', 66 | async t => { 67 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 68 | nock(domain).get('/.well-known/certs').reply(200, jwks) 69 | const getJwks = buildGetJwks({ providerDiscovery: true }) 70 | const key = jwks.keys[2] 71 | 72 | const jwk = await getJwks.getJwk({ domain, kid: key.kid }) 73 | 74 | t.assert.ok(jwk) 75 | t.assert.deepStrictEqual(jwk, key) 76 | } 77 | ) 78 | 79 | test( 80 | 'returns a jwk if no alg is provided and kid match for discovery but jwk has alg', 81 | async t => { 82 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 83 | nock(domain).get('/.well-known/certs').reply(200, jwks) 84 | const getJwks = buildGetJwks({ providerDiscovery: true }) 85 | const key = jwks.keys[1] 86 | 87 | const jwk = await getJwks.getJwk({ domain, kid: key.kid }) 88 | 89 | t.assert.ok(jwk) 90 | t.assert.deepStrictEqual(jwk, key) 91 | } 92 | ) 93 | 94 | test('caches a successful response for discovery', async t => { 95 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 96 | nock(domain).get('/.well-known/certs').reply(200, jwks) 97 | 98 | const getJwks = buildGetJwks({ providerDiscovery: true }) 99 | const key = jwks.keys[0] 100 | const { alg, kid } = key 101 | 102 | await getJwks.getJwk({ domain, alg, kid }) 103 | const jwk = await getJwks.getJwk({ domain, alg, kid }) 104 | 105 | t.assert.ok(jwk) 106 | t.assert.deepStrictEqual(jwk, key) 107 | }) 108 | 109 | test('does not cache a failed response for discovery', async t => { 110 | nock(domain) 111 | .get('/.well-known/openid-configuration') 112 | .twice() 113 | .reply(200, oidcConfig) 114 | nock(domain).get('/.well-known/certs').reply(500, { msg: 'boom' }) 115 | nock(domain).get('/.well-known/certs').reply(200, jwks) 116 | 117 | const [{ alg, kid }] = jwks.keys 118 | const getJwks = buildGetJwks({ providerDiscovery: true }) 119 | 120 | await t.assert.rejects(getJwks.getJwk({ domain, alg, kid })) 121 | await getJwks.getJwk({ domain, alg, kid }) 122 | }) 123 | 124 | test('rejects if response is an empty object for discovery', async t => { 125 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 126 | nock(domain).get('/.well-known/certs').reply(200, {}) 127 | const getJwks = buildGetJwks({ providerDiscovery: true }) 128 | const [{ alg, kid }] = jwks.keys 129 | 130 | return t.assert.rejects( 131 | getJwks.getJwk({ domain, alg, kid }), 132 | new GetJwksError('NO_JWKS', 'No JWKS found in the response.') 133 | ) 134 | }) 135 | 136 | test('rejects if no JWKS are found in the response', async t => { 137 | nock(domain).get('/.well-known/openid-configuration').reply(200, oidcConfig) 138 | nock(domain).get('/.well-known/certs').reply(200, { keys: [] }) 139 | const getJwks = buildGetJwks({ providerDiscovery: true }) 140 | const [{ alg, kid }] = jwks.keys 141 | 142 | return t.assert.rejects( 143 | getJwks.getJwk({ domain, alg, kid }), 144 | new GetJwksError('NO_JWKS', 'No JWKS found in the response.') 145 | ) 146 | }) 147 | 148 | test('supports domain without trailing slash for discovery', async t => { 149 | nock(domain) 150 | .get('/.well-known/openid-configuration') 151 | .once() 152 | .reply(200, oidcConfig) 153 | nock(domain).get('/.well-known/certs').reply(200, jwks) 154 | const getJwks = buildGetJwks({ providerDiscovery: true }) 155 | const [{ alg, kid }] = jwks.keys 156 | 157 | const key = await getJwks.getJwk({ domain: 'https://localhost', alg, kid }) 158 | t.assert.ok(key) 159 | }) 160 | 161 | test('does not execute concurrent requests for discovery', () => { 162 | nock(domain) 163 | .get('/.well-known/openid-configuration') 164 | .once() 165 | .reply(200, oidcConfig) 166 | nock(domain).get('/.well-known/certs').once().reply(200, jwks) 167 | 168 | const getJwks = buildGetJwks({ providerDiscovery: true }) 169 | const [{ alg, kid }] = jwks.keys 170 | 171 | return Promise.all([ 172 | getJwks.getJwk({ domain, alg, kid }), 173 | getJwks.getJwk({ domain, alg, kid }), 174 | ]) 175 | }) 176 | 177 | test( 178 | 'returns a stale cached value if request fails for discovery', 179 | async t => { 180 | // allow 2 requests, third will throw an error 181 | nock(domain) 182 | .get('/.well-known/openid-configuration') 183 | .thrice() 184 | .reply(200, oidcConfig) 185 | nock(domain).get('/.well-known/certs').twice().reply(200, jwks) 186 | nock(domain).get('/.well-known/certs').once().reply(500, { boom: true }) 187 | 188 | // allow only 1 entry in cache 189 | const getJwks = buildGetJwks({ providerDiscovery: true, max: 1 }) 190 | const [key1, key2] = jwks.keys 191 | 192 | // request key1 193 | await getJwks.getJwk({ 194 | domain: 'https://localhost', 195 | alg: key1.alg, 196 | kid: key1.kid, 197 | }) 198 | 199 | // request key2 200 | await getJwks.getJwk({ 201 | domain: 'https://localhost', 202 | alg: key2.alg, 203 | kid: key2.kid, 204 | }) 205 | 206 | // now key1 is stale 207 | // request key1 208 | const key = await getJwks.getJwk({ 209 | domain: 'https://localhost', 210 | alg: key1.alg, 211 | kid: key1.kid, 212 | }) 213 | 214 | t.assert.deepStrictEqual(key, key1) 215 | } 216 | ) 217 | 218 | describe('allowed domains for discovery', () => { 219 | test('allows any domain by default for discovery ', async t => { 220 | const domain = 'https://example.com' 221 | 222 | nock(domain) 223 | .get('/.well-known/openid-configuration') 224 | .reply(200, { 225 | issuer: domain, 226 | jwks_uri: `${domain}/.well-known/certs`, 227 | }) 228 | nock(domain).get('/.well-known/certs').reply(200, jwks) 229 | const getJwks = buildGetJwks({ providerDiscovery: true }) 230 | 231 | const [{ alg, kid }] = jwks.keys 232 | 233 | t.assert.ok(await getJwks.getJwk({ domain, alg, kid })) 234 | }) 235 | 236 | const allowedCombinations = [ 237 | // same without trailing slash 238 | ['https://example.com', 'https://example.com'], 239 | // same with trailing slash 240 | ['https://example.com/', 'https://example.com/'], 241 | // one without, one with 242 | ['https://example.com', 'https://example.com/'], 243 | // one with, one without 244 | ['https://example.com/', 'https://example.com'], 245 | ] 246 | 247 | allowedCombinations.forEach(([allowedIssuer, domainFromToken]) => { 248 | test( 249 | `allows domain ${allowedIssuer} requested with ${domainFromToken} for discovery`, 250 | async t => { 251 | const allowedIssuerSlash = allowedIssuer.endsWith('/') 252 | ? allowedIssuer 253 | : `${allowedIssuer}/` 254 | nock(allowedIssuer) 255 | .get('/.well-known/openid-configuration') 256 | .reply(200, { 257 | issuer: allowedIssuer, 258 | jwks_uri: `${allowedIssuerSlash}.well-known/certs`, 259 | }) 260 | nock(allowedIssuer).get('/.well-known/certs').reply(200, jwks) 261 | const getJwks = buildGetJwks({ 262 | providerDiscovery: true, 263 | issuersWhitelist: [allowedIssuer], 264 | }) 265 | 266 | const [{ alg, kid }] = jwks.keys 267 | 268 | t.assert.ok(await getJwks.getJwk({ domain: domainFromToken, alg, kid })) 269 | } 270 | ) 271 | }) 272 | 273 | test('allows multiple domains for discovery', async t => { 274 | const domain1 = 'https://example1.com' 275 | const domain2 = 'https://example2.com' 276 | nock(domain1) 277 | .get('/.well-known/openid-configuration') 278 | .reply(200, { 279 | issuer: domain, 280 | jwks_uri: `${domain1}/.well-known/certs`, 281 | }) 282 | nock(domain1).get('/.well-known/certs').reply(200, jwks) 283 | nock(domain2) 284 | .get('/.well-known/openid-configuration') 285 | .reply(200, { 286 | issuer: domain, 287 | jwks_uri: `${domain2}/.well-known/certs`, 288 | }) 289 | nock(domain2).get('/.well-known/certs').reply(200, jwks) 290 | 291 | const getJwks = buildGetJwks({ 292 | providerDiscovery: true, 293 | issuersWhitelist: [domain1, domain2], 294 | }) 295 | 296 | const [{ alg, kid }] = jwks.keys 297 | 298 | t.assert.ok(await getJwks.getJwk({ domain: domain1, alg, kid })) 299 | t.assert.ok(await getJwks.getJwk({ domain: domain2, alg, kid })) 300 | }) 301 | 302 | test('forbids domain outside of the allow list', async t => { 303 | const getJwks = buildGetJwks({ 304 | providerDiscovery: true, 305 | issuersWhitelist: ['https://example.com/'], 306 | }) 307 | 308 | const [{ alg, kid }] = jwks.keys 309 | 310 | return t.assert.rejects( 311 | getJwks.getJwk({ domain, alg, kid }), 312 | new GetJwksError('DOMAIN_NOT_ALLOWED', 'The domain is not allowed.') 313 | ) 314 | }) 315 | }) 316 | -------------------------------------------------------------------------------- /test/getJwksUri.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { beforeEach, afterEach, test, describe } = require('node:test') 4 | const nock = require('nock') 5 | 6 | const { domain } = require('./constants') 7 | 8 | const buildGetJwks = require('../src/get-jwks') 9 | const { GetJwksError, errorCode } = require('../src/error') 10 | 11 | beforeEach(() => { 12 | nock.disableNetConnect() 13 | }) 14 | 15 | afterEach(() => { 16 | nock.cleanAll() 17 | nock.enableNetConnect() 18 | }) 19 | 20 | test('throw error if the discovery request fails', async t => { 21 | nock(domain) 22 | .get('/.well-known/openid-configuration') 23 | .reply(500, { msg: 'baam' }) 24 | const getJwks = buildGetJwks({ providerDiscovery: true }) 25 | 26 | const expectedError = { 27 | name: GetJwksError.name, 28 | code: errorCode.OPENID_CONFIGURATION_REQUEST_FAILED, 29 | body: { msg: 'baam' }, 30 | } 31 | 32 | await t.assert.rejects(getJwks.getJwksUri(domain), expectedError) 33 | }) 34 | 35 | test( 36 | 'throw error if the discovery request has no jwks_uri property', 37 | async t => { 38 | nock(domain) 39 | .get('/.well-known/openid-configuration') 40 | .reply(200, { msg: 'baam' }) 41 | const getJwks = buildGetJwks({ providerDiscovery: true }) 42 | 43 | const expectedError = { 44 | name: GetJwksError.name, 45 | code: errorCode.NO_JWKS_URI, 46 | } 47 | 48 | await t.assert.rejects(getJwks.getJwksUri(domain), expectedError) 49 | }, 50 | ) 51 | 52 | describe('timeout', () => { 53 | let timeout 54 | 55 | beforeEach(() => { 56 | global.fetch = (url, options) => { 57 | timeout = options.timeout 58 | return Promise.resolve({ 59 | ok: true, 60 | json: () => Promise.resolve({ jwks_uri: 'http://localhost' }) 61 | }) 62 | } 63 | }) 64 | 65 | test('timeout defaults to 5 seconds', async t => { 66 | const getJwks = buildGetJwks() 67 | await getJwks.getJwksUri(domain) 68 | t.assert.equal(timeout, 5000) 69 | }) 70 | 71 | test('ensures that timeout is set to 10 seconds', async t => { 72 | const getJwks = buildGetJwks({fetchOptions: {timeout: 10000 }}) 73 | await getJwks.getJwksUri(domain) 74 | t.assert.equal(timeout, 10000) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /test/getPublicKey.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test} = require('node:test') 4 | const jwkToPem = require('jwk-to-pem') 5 | const sinon = require('sinon') 6 | 7 | const { jwks } = require('./constants') 8 | const buildGetJwks = require('../src/get-jwks') 9 | 10 | test('it provides the result of getJwk to jwkToPem', async t => { 11 | const getJwks = buildGetJwks() 12 | 13 | const [jwk] = jwks.keys 14 | 15 | const getJwkStub = sinon.stub(getJwks, 'getJwk').resolves(jwk) 16 | 17 | const signature = 'whatever' 18 | 19 | const pem = await getJwks.getPublicKey(signature) 20 | 21 | t.assert.equal(pem, jwkToPem(jwk)) 22 | sinon.assert.calledOnceWithExactly(getJwkStub, signature) 23 | }) 24 | 25 | test('it rejects if getJwk rejects', t => { 26 | const getJwks = buildGetJwks() 27 | 28 | sinon.stub(getJwks, 'getJwk').rejects(new Error('boom')) 29 | 30 | return t.assert.rejects(getJwks.getPublicKey('whatever'), new Error('boom')) 31 | }) 32 | -------------------------------------------------------------------------------- /test/getPublicKeyDiscovery.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {test} = require('node:test') 4 | const jwkToPem = require('jwk-to-pem') 5 | const sinon = require('sinon') 6 | 7 | const { jwks } = require('./constants') 8 | const buildGetJwks = require('../src/get-jwks') 9 | 10 | test( 11 | 'it provides the result of getJwk to jwkToPem for discovery', 12 | async t => { 13 | const getJwks = buildGetJwks({ providerDiscovery: true }) 14 | 15 | const [jwk] = jwks.keys 16 | 17 | const getJwkStub = sinon.stub(getJwks, 'getJwk').resolves(jwk) 18 | 19 | const signature = 'whatever' 20 | 21 | const pem = await getJwks.getPublicKey(signature) 22 | 23 | t.assert.equal(pem, jwkToPem(jwk)) 24 | sinon.assert.calledOnceWithExactly(getJwkStub, signature) 25 | } 26 | ) 27 | 28 | test('it rejects if getJwk rejects for discovery', t => { 29 | const getJwks = buildGetJwks({ providerDiscovery: true }) 30 | 31 | sinon.stub(getJwks, 'getJwk').rejects(new Error('boom')) 32 | 33 | return t.assert.rejects(getJwks.getPublicKey('whatever'), new Error('boom')) 34 | }) 35 | -------------------------------------------------------------------------------- /test/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEA7KRDtHuJ9+R1cYzB9+E4TUVazzv93MMmMo/38nOwEKNxlWs7 3 | OVg397d0SCsdmBbcbr4KTMeblY4a+VOzLVZ5ycYgi7ZbMvv7RzunKuPsjm7m863d 4 | LnPUFOptsFVANDOHgDYopKBFYoIMoxjXU7bOzLL+Ez0oO5keT1hGZkJT/7GRvKyY 5 | igugN4lLia4Tb3AmUN60wiloyQCJ2xYATWHB0e4sTwIDq6MFXhVFHXV6ZBU7sDh0 6 | HqmP08gJtMnsFOE7zUcbpqTvpz5nAR6EyUs7R0g61WmGUfQTrE6byVCZ8w0NN4Xe 7 | r6IQBjnDZWbmf69jsAFFAYDCe+omWXY526qLQwIDAQABAoIBAQDhLz8uVBDqUABi 8 | WWuLEkqdXU4YviHJHfsPSmjL0sLMUnwXj77/xq8bjvucYUr8G2UQDM+IWLn5Cw0o 9 | DToH/q5OD7eQu6r1TUvEcUOWUOYec/JaGCzNs3MxpBNVJQq/ofljTCZI4iqkntSf 10 | r1fYVbFcoUedzyil7gMlxf7X+G4udVpL4qF/rqeMg+8RoQVodEULvYUKC4iGvDMn 11 | 0/euHcH+Ih97pyWyCe8AoYifsBDkIYcQmX0jTnqxXcUEitiXj/bcewJXDUu2hNOK 12 | Vr1eIJnKTZdvXQHF2UoBkQln5cBeAyNHT3QOpONo9Xed77ubwr/j/hU0A0lroNAl 13 | zhUknOQhAoGBAP/djXqYzyu+6qKVLOGjR7r0Vz/yhdqCwlDLM7x4crCHkne8upzg 14 | zw6x71W+mVXjZW44tSzRVaBEpPuXRq6b93yh8haYBvpWJ+AlJD3/XGpylBvdjnDK 15 | gCM87cr/GkT+egEQysThUswANJR4KPIvkmBM4dcfu3QCEJedaYulqvBVAoGBAOzE 16 | H6tnvJkCK4KXmfnRC0jdEZNrVBLe7VkUlV+Wx37cGWmK2K2B2APUnuCrfOwVbFIh 17 | KL/aOiYKWUNne+2+oQOKCI7TmyOfA4mBnEqLnKbZoc5LTI5iDXbaBzvXuqhJVce4 18 | YQqLGYxgr7dcVy9kqUFtFNmaaJhJgEBm1qGOgkU3AoGBAMC3TSK0Cga3C99dYKqq 19 | 4xIri7P8pVkJ9/YGt3cTeb8Avg81tZEHuq0k1FHO94s7dWBpkfypx0aprWJadMB7 20 | dRMIn2DpLQhM8EfhccTInAEJQAkk/W5y98SS1cB6GH0y9w3qae+Uj1pcJT5WqvCP 21 | aD7kaY4wtm4QSBMKWz71jyTpAoGBAJbTtW0Gr5E1XaxakR8geTTYh3rG84717nNB 22 | 9oonTjzVT2b5qWCWh5qhFvj+pZzrZM7JCuF0zngvPX//62Wfe4j6pMr/qCPAB4vQ 23 | QlUGrStpFneJZmKJuhQNfnAz1FeiKAALx93kkMjpSube7zdkw6HHMHISuDDTGd1s 24 | 5auTUg9vAoGBAO8Mg1icLrcz1hcq2VcvD1jArMEj4KLJKbhUJdtmnO1sB2W8S3NY 25 | 8Ea83W/KVXSwHo3uaQu3Gpl/iA7ScJR8Bbxwa7oYZxrf3Vm8c63Twuy9rG/K5TPU 26 | FNCax9JdPk5d+ufYUrVCccQaTajUcQmCEwVuyRBos2MkyVukykff+/vD 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7KRDtHuJ9+R1cYzB9+E4 3 | TUVazzv93MMmMo/38nOwEKNxlWs7OVg397d0SCsdmBbcbr4KTMeblY4a+VOzLVZ5 4 | ycYgi7ZbMvv7RzunKuPsjm7m863dLnPUFOptsFVANDOHgDYopKBFYoIMoxjXU7bO 5 | zLL+Ez0oO5keT1hGZkJT/7GRvKyYigugN4lLia4Tb3AmUN60wiloyQCJ2xYATWHB 6 | 0e4sTwIDq6MFXhVFHXV6ZBU7sDh0HqmP08gJtMnsFOE7zUcbpqTvpz5nAR6EyUs7 7 | R0g61WmGUfQTrE6byVCZ8w0NN4Xer6IQBjnDZWbmf69jsAFFAYDCe+omWXY526qL 8 | QwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /test/types/get-jwks.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable } from 'tsd' 2 | import { getGlobalDispatcher } from 'undici' 3 | import Cache from '../../src/cache' 4 | import buildGetJwks, { GetJwksOptions, GetPublicKeyOptions, JWK, JWKSignature } from '../../src/get-jwks' 5 | 6 | const { getPublicKey, getJwk, getJwksUri, cache, staleCache } = buildGetJwks() 7 | 8 | expectAssignable<(signature: JWKSignature) => Promise>(getJwk) 9 | expectAssignable<(normalizedDomain: string) => Promise>(getJwksUri) 10 | expectAssignable<(options?: GetPublicKeyOptions) => Promise>(getPublicKey) 11 | expectAssignable>(cache) 12 | expectAssignable>(staleCache) 13 | expectAssignable({ 14 | dispatcher: getGlobalDispatcher() 15 | }) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "outDir": "lib", 8 | "declarationDir": "types", 9 | "allowJs": false, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "strict": true, 13 | "noImplicitAny": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strictNullChecks": true, 17 | "declaration": true 18 | }, 19 | "files": ["./test/types/get-jwks.test-d.ts"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------