mirror of
				https://github.com/therootcompany/keyfetch.js.git
				synced 2024-11-16 17:29:02 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			308 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # [keyfetch](https://git.rootprojects.org/root/keyfetch.js)
 | |
| 
 | |
| 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, Okta)
 | |
| -   [x] Other JWKs URLs
 | |
| 
 | |
| Crypto Support
 | |
| 
 | |
| -   [x] JWT verification
 | |
| -   [x] RSA (all variants)
 | |
| -   [x] EC / ECDSA (NIST variants P-256, P-384)
 | |
| -   [x] Sane error codes
 | |
| -   [ ] esoteric variants (excluded to keep the code featherweight and secure)
 | |
| 
 | |
| # Table of Contents
 | |
| 
 | |
| -   Install
 | |
| -   Usage
 | |
| -   API
 | |
|     -   Auth0 / Okta
 | |
|     -   OIDC
 | |
| -   Errors
 | |
| -   Change Log
 | |
| 
 | |
| # 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 (for authentication):
 | |
| 
 | |
| ```js
 | |
| var keyfetch = require("keyfetch");
 | |
| var jwt = "...";
 | |
| 
 | |
| keyfetch.jwt.verify(jwt).then(function (decoded) {
 | |
|     console.log(decoded);
 | |
| });
 | |
| ```
 | |
| 
 | |
| JWT verification (for authorization):
 | |
| 
 | |
| ```js
 | |
| var options = { issuers: ["https://example.com/"], claims: { role: "admin" } };
 | |
| keyfetch.jwt.verify(jwt, options).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.jwt.verify(jwt, { jwk: result.jwk });
 | |
| });
 | |
| ```
 | |
| 
 | |
| _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) && /(\.|^)example\.com$/.test(iss) // must be a secure domain
 | |
|     ); // 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
 | |
| 
 | |
| This can accept a _JWT string_ (compact JWS) or a _decoded JWT object_ (JWS).
 | |
| 
 | |
| This can be used purely for verifying pure authentication tokens, as well as authorization tokens.
 | |
| 
 | |
| ```js
 | |
| keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
 | |
|     /*
 | |
|     { protected: '...'  // base64 header
 | |
|     , payload: '...'    // base64 payload
 | |
|     , signature: '...'  // base64 signature
 | |
|     , header: {...}     // decoded header
 | |
|     , claims: {...}     // decoded payload
 | |
|     }
 | |
|   */
 | |
| });
 | |
| ```
 | |
| 
 | |
| When used for authorization, it's important to specify a limited set of trusted `issuers`. \
 | |
| When using for federated authentication you may set `issuers = ["*"]` - but **DO NOT** trust claims such as `email` and `email_verified`.
 | |
| 
 | |
| If your authorization `claims` can be expressed as exact string matches, you can specify those too.
 | |
| 
 | |
| ```js
 | |
| keyfetch.jwt.verify(jwt, {
 | |
|   strategy: 'oidc',
 | |
|   issuers: [ 'https://example.com/' ],
 | |
|   //iss: 'https://example.com/',
 | |
|   claims: { role: 'admin', sub: 'abc', group: 'xyz' }
 | |
| }).then(function (verified) {
 | |
| ```
 | |
| 
 | |
| -   `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url.
 | |
| -   `issuers` must be a list of https urls (though http is allowed for things like Docker swarm), or '\*'
 | |
| -   `iss` is like `issuers`, but only one
 | |
| -   `claims` is an object with arbitrary keys (i.e. everything except for the standard `iat`, `exp`, `jti`, etc)
 | |
| -   `exp` may be set to `false` if you're validating on your own (i.e. allowing time drift leeway)
 | |
| -   `jwks` can be used to specify a list of allowed public key rather than fetching them (i.e. for offline unit tests)
 | |
| -   `jwk` same as above, but a single key rather than a list
 | |
| 
 | |
| ### Decode JWT
 | |
| 
 | |
| ```jwt
 | |
| try {
 | |
|   console.log( keyfetch.jwt.decode(jwt) );
 | |
| } catch(e) {
 | |
|   console.error(e);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ```js
 | |
| { protected: '...'  // base64 header
 | |
| , payload: '...'    // base64 payload
 | |
| , signature: '...'  // base64 signature
 | |
| , header: {...}     // decoded header
 | |
| , claims: {...}     // decoded payload
 | |
| ```
 | |
| 
 | |
| It's easier just to show the code than to explain the example.
 | |
| 
 | |
| ```js
 | |
| keyfetch.jwt.decode = function (jwt) {
 | |
|     // Unpack JWS from "compact" form
 | |
|     var parts = jwt.split(".");
 | |
|     var obj = {
 | |
|         protected: parts[0],
 | |
|         payload: parts[1],
 | |
|         signature: parts[2]
 | |
|     };
 | |
| 
 | |
|     // Decode JWT properties from JWS as unordered objects
 | |
|     obj.header = JSON.parse(Buffer.from(obj.protected, "base64"));
 | |
|     obj.claims = JSON.parse(Buffer.from(obj.payload, "base64"));
 | |
| 
 | |
|     return obj;
 | |
| };
 | |
| ```
 | |
| 
 | |
| ### 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.
 | |
| 
 | |
| # Errors
 | |
| 
 | |
| `JSON.stringify()`d errors look like this:
 | |
| 
 | |
| ```js
 | |
| {
 | |
|   code: "INVALID_JWT",
 | |
|   status: 401,
 | |
|   details: [ "jwt.claims.exp = 1634804500", "DEBUG: helpful message" ]
 | |
|   message: "token's 'exp' has passed or could not parsed: 1634804500"
 | |
| }
 | |
| ```
 | |
| 
 | |
| SemVer Compatibility:
 | |
| 
 | |
| -   `code` & `status` will remain the same.
 | |
| -   The `message` property of an error is **NOT** included in the semver compatibility guarantee (we intend to make them more client-friendly), neither is `detail` at this time (but it will be once we decide on what it should be).
 | |
| 
 | |
| For backwards compatibility with v1, the non-stringified `message` is the same as what it was in v1 (and the v2 message is `client_message`, which replaces `message` in v3). Don't rely on it. Rely on `code`.
 | |
| 
 | |
| | Hint                | Code            | Status | Message (truncated)                              |
 | |
| | ------------------- | --------------- | ------ | ------------------------------------------------ |
 | |
| | (developer error)   | DEVELOPER_ERROR | 500    | test...                                          |
 | |
| | (bad gateway)       | BAD_GATEWAY     | 502    | The token could not be verified because our s... |
 | |
| | (insecure issuer)   | MALFORMED_JWT   | 400    | 'test' is NOT secure. Set env 'KEYFETCH_ALLOW... |
 | |
| | (parse error)       | MALFORMED_JWT   | 400    | could not parse jwt: 'test'...                   |
 | |
| | (no issuer)         | MALFORMED_JWT   | 400    | 'iss' is not defined...                          |
 | |
| | (malformed exp)     | MALFORMED_JWT   | 400    | token's 'exp' has passed or could not parsed:... |
 | |
| | (expired)           | INVALID_JWT     | 401    | token's 'exp' has passed or could not parsed:... |
 | |
| | (inactive)          | INVALID_JWT     | 401    | token's 'nbf' has not been reached or could n... |
 | |
| | (bad signature)     | INVALID_JWT     | 401    | token signature verification was unsuccessful... |
 | |
| | (jwk not found old) | INVALID_JWT     | 401    | Retrieved a list of keys, but none of them ma... |
 | |
| | (jwk not found)     | INVALID_JWT     | 401    | No JWK found by kid or thumbprint 'test'...      |
 | |
| | (no jwkws uri)      | INVALID_JWT     | 401    | Failed to retrieve openid configuration...       |
 | |
| | (unknown issuer)    | INVALID_JWT     | 401    | token was issued by an untrusted issuer: 'tes... |
 | |
| | (failed claims)     | INVALID_JWT     | 401    | token did not match on one or more authorizat... |
 | |
| 
 | |
| # Change Log
 | |
| 
 | |
| Minor Breaking changes (with a major version bump):
 | |
| 
 | |
| -   v3.0.0 - reworked error messages (also available in v2.1.0 as `client_message`)
 | |
| -   v2.0.0 - changes from the default `issuers = ["*"]` to requiring that an issuer (or public jwk for verification) is specified
 | |
| 
 | |
| See other changes in [CHANGELOG.md](./CHANGELOG.md).
 |