174 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			174 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | # keyfetch
 | ||
|  | 
 | ||
|  | Lightweight support for fetching JWKs. | ||
|  | 
 | ||
|  | Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `jsonwebtoken` package | ||
|  | (and node's native RSA and ECDSA crypto APIs). | ||
|  | 
 | ||
|  | ## Features
 | ||
|  | 
 | ||
|  | Works great for | ||
|  | 
 | ||
|  | * [x] `jsonwebtoken` (Auth0) | ||
|  | * [x] OIDC (OpenID Connect) | ||
|  | * [x] .well-known/jwks.json (Auth0) | ||
|  | * [x] Other JWKs URLs | ||
|  | 
 | ||
|  | Crypto Support | ||
|  | 
 | ||
|  | * [x] JWT verification | ||
|  | * [x] RSA (all variants) | ||
|  | * [x] EC / ECDSA (NIST variants P-256, P-384) | ||
|  | * [ ] esoteric variants (excluded to keep the code featherweight and secure) | ||
|  | 
 | ||
|  | # Install
 | ||
|  | 
 | ||
|  | ```bash | ||
|  | npm install --save keyfetch | ||
|  | ``` | ||
|  | 
 | ||
|  | # Usage
 | ||
|  | 
 | ||
|  | Retrieve a key list of keys: | ||
|  | 
 | ||
|  | ```js | ||
|  | var keyfetch = require('keyfetch'); | ||
|  | 
 | ||
|  | keyfetch.oidcJwks("https://example.com/").then(function (results) { | ||
|  |   results.forEach(function (result) { | ||
|  |     console.log(result.jwk); | ||
|  |     console.log(result.thumprint); | ||
|  |     console.log(result.pem); | ||
|  |   }); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | Quick JWT verification: | ||
|  | 
 | ||
|  | ```js | ||
|  | var keyfetch = require('keyfetch'); | ||
|  | var jwt = '...'; | ||
|  | 
 | ||
|  | keyfetch.verify({ jwt: jwt }).then(function (decoded) { | ||
|  |   console.log(decoded); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | Verify a JWT with `jsonwebtoken`: | ||
|  | 
 | ||
|  | ```js | ||
|  | var keyfetch = require('keyfetch'); | ||
|  | var jwt = require('jsonwebtoken'); | ||
|  | var auth = "..."; // some JWT | ||
|  | var token = jwt.decode(auth, { json: true, complete: true }) | ||
|  | 
 | ||
|  | if (!isTrustedIssuer(token.payload.iss)) { | ||
|  |   throw new Error("untrusted issuer"); | ||
|  | } | ||
|  | 
 | ||
|  | keyfetch.oidcJwk( | ||
|  |   token.header.kid | ||
|  | , token.payload.iss | ||
|  | ).then(function (result) { | ||
|  |   console.log(result.jwk); | ||
|  |   console.log(result.thumprint); | ||
|  |   console.log(result.pem); | ||
|  | 
 | ||
|  |   jwt.verify(jwt, pem); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | *Note*: You might implement `isTrustedIssuer` one of these: | ||
|  | 
 | ||
|  | ```js | ||
|  | function isTrustedIssuer(iss) { | ||
|  |   return -1 !== [ 'https://partner.com/', 'https://auth0.com/'].indexOf(iss); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | ```js | ||
|  | function isTrustedIssuer(iss) { | ||
|  |   return /^https:/.test(iss) &&         // must be a secure domain | ||
|  |     /(\.|^)example\.com$/.test(iss);    // can be example.com or any subdomain | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | # API
 | ||
|  | 
 | ||
|  | All API calls will return the RFC standard JWK SHA256 thumbprint as well as a PEM version of the key. | ||
|  | 
 | ||
|  | Note: When specifying `id`, it may be either `kid` (as in `token.header.kid`) | ||
|  | or `thumbprint` (as in `result.thumbprint`). | ||
|  | 
 | ||
|  | ### JWKs URLs
 | ||
|  | 
 | ||
|  | Retrieves keys from a URL such as `https://example.com/jwks/` with the format `{ keys: [ { kid, kty, exp, ... } ] }` | ||
|  | and returns the array of keys (as well as thumbprint and jwk-to-pem). | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.jwks(jwksUrl) | ||
|  | // Promises [ { jwk, thumbprint, pem } ] or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.jwk(id, jwksUrl) | ||
|  | // Promises { jwk, thumbprint, pem } or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Auth0
 | ||
|  | 
 | ||
|  | If `https://example.com/` is used as `issuerUrl` it will resolve to | ||
|  | `https://example.com/.well-known/jwks.json` and return the keys. | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.wellKnownJwks(issuerUrl) | ||
|  | // Promises [ { jwk, thumbprint, pem } ] or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.wellKnownJwk(id, issuerUrl) | ||
|  | // Promises { jwk, thumbprint, pem } or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ### OIDC
 | ||
|  | 
 | ||
|  | If `https://example.com/` is used as `issuerUrl` then it will first resolve to | ||
|  | `https://example.com/.well-known/openid-configuration` and then follow `jwks_uri` to return the keys. | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.oidcJwks(issuerUrl) | ||
|  | // Promises [ { jwk, thumbprint, pem } ] or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.oidcJwk(id, issuerUrl) | ||
|  | // Promises { jwk, thumbprint, pem } or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Verify JWT
 | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.verify({ jwt: jwk, strategy: 'oidc' }) | ||
|  | // Promises a decoded JWT { headers, payload, signature } or fails | ||
|  | ``` | ||
|  | 
 | ||
|  | * `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url. | ||
|  | 
 | ||
|  | ### Cache Settings
 | ||
|  | 
 | ||
|  | ```js | ||
|  | keyfetch.init({ | ||
|  |   // set all keys at least 1 hour (regardless of jwk.exp) | ||
|  |   mincache: 1 * 60 * 60 | ||
|  | 
 | ||
|  |   // expire each key after 3 days (regardless of jwk.exp) | ||
|  | , maxcache: 3 * 24 * 60 * 60 | ||
|  | 
 | ||
|  |   // re-fetch a key up to 15 minutes before it expires (only if used) | ||
|  | , staletime: 15 * 60 | ||
|  | }) | ||
|  | ``` | ||
|  | 
 | ||
|  | There is no background task to cleanup expired keys as of yet. | ||
|  | For now you can limit the number of keys fetched by having a simple whitelist. |