mirror of
				https://github.com/therootcompany/keyfetch.js.git
				synced 2024-11-16 17:29:02 +00:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a84e833571 | |||
| 076246e4d0 | |||
| 4c85fc4009 | |||
| d5647ea905 | |||
| 2ea44e3a46 | |||
| 5ef53ecb23 | |||
| 604b42c7ef | |||
| c57a08f0cd | |||
| a3539b0941 | |||
| 523a4f0d1a | |||
| 7d5889b4de | |||
| 79e6758f9f | |||
| 842807f92b | |||
| a648de58d8 | |||
| 8ec2d98645 | |||
| 9141962456 | |||
| aba70bf0ff | |||
| a32b942861 | |||
| e274e5368a | |||
| e6de23532b | |||
| 5060c505b6 | |||
| 9b77939455 | |||
| 029bc13fe8 | |||
| 5fef6a7430 | |||
| 448b977963 | |||
| 2c36227afd | |||
| 9e5ffd1fc9 | |||
| 0f0d0807a0 | |||
| cfe814ebb0 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | node_modules | ||||||
							
								
								
									
										22
									
								
								.jshintrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.jshintrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | { | ||||||
|  |     "browser": true, | ||||||
|  |     "node": true, | ||||||
|  |     "esversion": 11, | ||||||
|  |     "curly": true, | ||||||
|  |     "sub": true, | ||||||
|  |     "bitwise": true, | ||||||
|  |     "eqeqeq": true, | ||||||
|  |     "forin": true, | ||||||
|  |     "freeze": true, | ||||||
|  |     "immed": true, | ||||||
|  |     "latedef": "nofunc", | ||||||
|  |     "nonbsp": true, | ||||||
|  |     "nonew": true, | ||||||
|  |     "plusplus": true, | ||||||
|  |     "undef": true, | ||||||
|  |     "unused": "vars", | ||||||
|  |     "strict": true, | ||||||
|  |     "maxdepth": 3, | ||||||
|  |     "maxstatements": 100, | ||||||
|  |     "maxcomplexity": 10 | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "printWidth": 120, | ||||||
|  |   "tabWidth": 4, | ||||||
|  |   "trailingComma": "none", | ||||||
|  |   "useTabs": false | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | # v3.0.0 | ||||||
|  | 
 | ||||||
|  | **Breaking Change**: Standardize error `message`s (now they're more client-friendly). | ||||||
|  | 
 | ||||||
|  | # v2.1.0 | ||||||
|  | 
 | ||||||
|  | Feature: Add `code`, `status`, and `details` to errors. | ||||||
|  | 
 | ||||||
|  | # v2.0.0 | ||||||
|  | 
 | ||||||
|  | **Breaking Change**: require `issuers` array (rather than `["*"]` by default). | ||||||
							
								
								
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | Copyright 2019 AJ ONeal | ||||||
|  | 
 | ||||||
|  | This is open source software; you can redistribute it and/or modify it under the | ||||||
|  | terms of either: | ||||||
|  | 
 | ||||||
|  |    a) the "MIT License" | ||||||
|  |    b) the "Apache-2.0 License" | ||||||
|  | 
 | ||||||
|  | MIT License | ||||||
|  | 
 | ||||||
|  |    Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  |    of this software and associated documentation files (the "Software"), to deal | ||||||
|  |    in the Software without restriction, including without limitation the rights | ||||||
|  |    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  |    copies of the Software, and to permit persons to whom the Software is | ||||||
|  |    furnished to do so, subject to the following conditions: | ||||||
|  | 
 | ||||||
|  |    The above copyright notice and this permission notice shall be included in all | ||||||
|  |    copies or substantial portions of the Software. | ||||||
|  | 
 | ||||||
|  |    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  |    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  |    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  |    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  |    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  |    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  |    SOFTWARE. | ||||||
|  | 
 | ||||||
|  | Apache-2.0 License Summary | ||||||
|  | 
 | ||||||
|  |    Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |    you may not use this file except in compliance with the License. | ||||||
|  |    You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |      http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  | 
 | ||||||
|  |    Unless required by applicable law or agreed to in writing, software | ||||||
|  |    distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |    See the License for the specific language governing permissions and | ||||||
|  |    limitations under the License. | ||||||
							
								
								
									
										239
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										239
									
								
								README.md
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| # keyfetch | # [keyfetch](https://git.rootprojects.org/root/keyfetch.js) | ||||||
| 
 | 
 | ||||||
| Lightweight support for fetching JWKs. | Lightweight support for fetching JWKs. | ||||||
| 
 | 
 | ||||||
| @ -9,17 +9,28 @@ Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `j | |||||||
| 
 | 
 | ||||||
| Works great for | Works great for | ||||||
| 
 | 
 | ||||||
| * [x] `jsonwebtoken` (Auth0) | -   [x] `jsonwebtoken` (Auth0) | ||||||
| * [x] OIDC (OpenID Connect) | -   [x] OIDC (OpenID Connect) | ||||||
| * [x] .well-known/jwks.json (Auth0) | -   [x] .well-known/jwks.json (Auth0, Okta) | ||||||
| * [x] Other JWKs URLs | -   [x] Other JWKs URLs | ||||||
| 
 | 
 | ||||||
| Crypto Support | Crypto Support | ||||||
| 
 | 
 | ||||||
| * [x] JWT verification | -   [x] JWT verification | ||||||
| * [x] RSA (all variants) | -   [x] RSA (all variants) | ||||||
| * [x] EC / ECDSA (NIST variants P-256, P-384) | -   [x] EC / ECDSA (NIST variants P-256, P-384) | ||||||
| * [ ] esoteric variants (excluded to keep the code featherweight and secure) | -   [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 | # Install | ||||||
| 
 | 
 | ||||||
| @ -32,64 +43,71 @@ npm install --save keyfetch | |||||||
| Retrieve a key list of keys: | Retrieve a key list of keys: | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var keyfetch = require('keyfetch'); | var keyfetch = require("keyfetch"); | ||||||
| 
 | 
 | ||||||
| keyfetch.oidcJwks("https://example.com/").then(function (results) { | keyfetch.oidcJwks("https://example.com/").then(function (results) { | ||||||
|   results.forEach(function (result) { |     results.forEach(function (result) { | ||||||
|     console.log(result.jwk); |         console.log(result.jwk); | ||||||
|     console.log(result.thumprint); |         console.log(result.thumprint); | ||||||
|     console.log(result.pem); |         console.log(result.pem); | ||||||
|   }); |     }); | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Quick JWT verification: | Quick JWT verification (for authentication): | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var keyfetch = require('keyfetch'); | var keyfetch = require("keyfetch"); | ||||||
| var jwt = '...'; | var jwt = "..."; | ||||||
| 
 | 
 | ||||||
| keyfetch.verify({ jwt: jwt }).then(function (decoded) { | keyfetch.jwt.verify(jwt).then(function (decoded) { | ||||||
|   console.log(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`: | Verify a JWT with `jsonwebtoken`: | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var keyfetch = require('keyfetch'); | var keyfetch = require("keyfetch"); | ||||||
| var jwt = require('jsonwebtoken'); | var jwt = require("jsonwebtoken"); | ||||||
| var auth = "..."; // some JWT | var auth = "..."; // some JWT | ||||||
| var token = jwt.decode(auth, { json: true, complete: true }) | var token = jwt.decode(auth, { json: true, complete: true }); | ||||||
| 
 | 
 | ||||||
| if (!isTrustedIssuer(token.payload.iss)) { | if (!isTrustedIssuer(token.payload.iss)) { | ||||||
|   throw new Error("untrusted issuer"); |     throw new Error("untrusted issuer"); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| keyfetch.oidcJwk( | keyfetch.oidcJwk(token.header.kid, token.payload.iss).then(function (result) { | ||||||
|   token.header.kid |     console.log(result.jwk); | ||||||
| , token.payload.iss |     console.log(result.thumprint); | ||||||
| ).then(function (result) { |     console.log(result.pem); | ||||||
|   console.log(result.jwk); |  | ||||||
|   console.log(result.thumprint); |  | ||||||
|   console.log(result.pem); |  | ||||||
| 
 | 
 | ||||||
|   jwt.verify(jwt, pem); |     jwt.jwt.verify(jwt, { jwk: result.jwk }); | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| *Note*: You might implement `isTrustedIssuer` one of these: | _Note_: You might implement `isTrustedIssuer` one of these: | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| function isTrustedIssuer(iss) { | function isTrustedIssuer(iss) { | ||||||
|   return -1 !== [ 'https://partner.com/', 'https://auth0.com/'].indexOf(iss); |     return -1 !== ["https://partner.com/", "https://auth0.com/"].indexOf(iss); | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| function isTrustedIssuer(iss) { | function isTrustedIssuer(iss) { | ||||||
|   return /^https:/.test(iss) &&         // must be a secure domain |     return ( | ||||||
|     /(\.|^)example\.com$/.test(iss);    // can be example.com or any subdomain |         /^https:/.test(iss) && /(\.|^)example\.com$/.test(iss) // must be a secure domain | ||||||
|  |     ); // can be example.com or any subdomain | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -106,12 +124,12 @@ Retrieves keys from a URL such as `https://example.com/jwks/` with the format `{ | |||||||
| and returns the array of keys (as well as thumbprint and jwk-to-pem). | and returns the array of keys (as well as thumbprint and jwk-to-pem). | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.jwks(jwksUrl) | keyfetch.jwks(jwksUrl); | ||||||
| // Promises [ { jwk, thumbprint, pem } ] or fails | // Promises [ { jwk, thumbprint, pem } ] or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.jwk(id, jwksUrl) | keyfetch.jwk(id, jwksUrl); | ||||||
| // Promises { jwk, thumbprint, pem } or fails | // Promises { jwk, thumbprint, pem } or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -121,12 +139,12 @@ If `https://example.com/` is used as `issuerUrl` it will resolve to | |||||||
| `https://example.com/.well-known/jwks.json` and return the keys. | `https://example.com/.well-known/jwks.json` and return the keys. | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.wellKnownJwks(issuerUrl) | keyfetch.wellKnownJwks(issuerUrl); | ||||||
| // Promises [ { jwk, thumbprint, pem } ] or fails | // Promises [ { jwk, thumbprint, pem } ] or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.wellKnownJwk(id, issuerUrl) | keyfetch.wellKnownJwk(id, issuerUrl); | ||||||
| // Promises { jwk, thumbprint, pem } or fails | // Promises { jwk, thumbprint, pem } or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -136,38 +154,155 @@ 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. | `https://example.com/.well-known/openid-configuration` and then follow `jwks_uri` to return the keys. | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.oidcJwks(issuerUrl) | keyfetch.oidcJwks(issuerUrl); | ||||||
| // Promises [ { jwk, thumbprint, pem } ] or fails | // Promises [ { jwk, thumbprint, pem } ] or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.oidcJwk(id, issuerUrl) | keyfetch.oidcJwk(id, issuerUrl); | ||||||
| // Promises { jwk, thumbprint, pem } or fails | // Promises { jwk, thumbprint, pem } or fails | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Verify JWT | ### 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 | ```js | ||||||
| keyfetch.verify({ jwt: jwk, strategy: 'oidc' }) | keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) { | ||||||
| // Promises a decoded JWT { headers, payload, signature } or fails |     /* | ||||||
|  |     { protected: '...'  // base64 header | ||||||
|  |     , payload: '...'    // base64 payload | ||||||
|  |     , signature: '...'  // base64 signature | ||||||
|  |     , header: {...}     // decoded header | ||||||
|  |     , claims: {...}     // decoded payload | ||||||
|  |     } | ||||||
|  |   */ | ||||||
|  | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| * `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url. | 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 | ### Cache Settings | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| keyfetch.init({ | keyfetch.init({ | ||||||
|   // set all keys at least 1 hour (regardless of jwk.exp) |     // set all keys at least 1 hour (regardless of jwk.exp) | ||||||
|   mincache: 1 * 60 * 60 |     mincache: 1 * 60 * 60, | ||||||
| 
 | 
 | ||||||
|   // expire each key after 3 days (regardless of jwk.exp) |     // expire each key after 3 days (regardless of jwk.exp) | ||||||
| , maxcache: 3 * 24 * 60 * 60 |     maxcache: 3 * 24 * 60 * 60, | ||||||
| 
 | 
 | ||||||
|   // re-fetch a key up to 15 minutes before it expires (only if used) |     // re-fetch a key up to 15 minutes before it expires (only if used) | ||||||
| , staletime: 15 * 60 |     staletime: 15 * 60 | ||||||
| }) | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| There is no background task to cleanup expired keys as of yet. | 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. | 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. | ||||||
|  | -   `message` 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). | ||||||
|  | -   `details` may be added to, but not subtracted from | ||||||
|  | 
 | ||||||
|  | | Hint              | Code          | Status | Message (truncated)                                    | | ||||||
|  | | ----------------- | ------------- | ------ | ------------------------------------------------------ | | ||||||
|  | | bad gateway       | BAD_GATEWAY   | 502    | The auth token could not be verified because our se... | | ||||||
|  | | insecure issuer   | MALFORMED_JWT | 400    | The auth token could not be verified because our se... | | ||||||
|  | | parse error       | MALFORMED_JWT | 400    | The auth token could not be verified because it is ... | | ||||||
|  | | no issuer         | MALFORMED_JWT | 400    | The auth token could not be verified because it doe... | | ||||||
|  | | malformed exp     | MALFORMED_JWT | 400    | The auth token could not be verified because it's e... | | ||||||
|  | | expired           | INVALID_JWT   | 401    | The auth token is expired. To try again, go to the ... | | ||||||
|  | | inactive          | INVALID_JWT   | 401    | The auth token isn't valid yet. It's activation dat... | | ||||||
|  | | bad signature     | INVALID_JWT   | 401    | The auth token did not pass verification because it... | | ||||||
|  | | jwk not found old | INVALID_JWT   | 401    | The auth token did not pass verification because ou... | | ||||||
|  | | jwk not found     | INVALID_JWT   | 401    | The auth token did not pass verification because ou... | | ||||||
|  | | no jwkws uri      | INVALID_JWT   | 401    | The auth token did not pass verification because it... | | ||||||
|  | | unknown issuer    | INVALID_JWT   | 401    | The auth token did not pass verification because it... | | ||||||
|  | | failed claims     | INVALID_JWT   | 401    | The auth token did not pass verification because it... | | ||||||
|  | 
 | ||||||
|  | # 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`) | ||||||
|  |     -   started using `let` and template strings (drops _really_ old node compat) | ||||||
|  | -   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). | ||||||
|  | |||||||
							
								
								
									
										164
									
								
								keyfetch-test.js
									
									
									
									
									
								
							
							
						
						
									
										164
									
								
								keyfetch-test.js
									
									
									
									
									
								
							| @ -1,20 +1,164 @@ | |||||||
| 'use strict'; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var keyfetch = require('./keyfetch.js'); | var keyfetch = require("./keyfetch.js"); | ||||||
|  | var testIss = "https://example.auth0.com"; | ||||||
| 
 | 
 | ||||||
| keyfetch.init({}); | keyfetch.init({}); | ||||||
| keyfetch.oidcJwks("https://bigsquid.auth0.com").then(function (jwks) { | keyfetch | ||||||
|   console.log(jwks); |     .oidcJwks(testIss) | ||||||
|   return keyfetch.oidcJwk(jwks[0].thumbprint, "https://bigsquid.auth0.com").then(function (jwk) { |     .then(function (hits) { | ||||||
|     console.log(jwk); |         keyfetch._clear(); | ||||||
|   }); |         //console.log(hits);
 | ||||||
| }).catch(function (err) { |         return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () { | ||||||
|   console.error(err); |             return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function (/*jwk*/) { | ||||||
| }); |                 //console.log(jwk);
 | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     }) | ||||||
|  |     .then(function () { | ||||||
|  |         console.log("Fetching PASSES"); | ||||||
|  |     }) | ||||||
|  |     .catch(function (err) { | ||||||
|  |         console.error("NONE SHALL PASS!"); | ||||||
|  |         console.error(err); | ||||||
|  |         process.exit(1); | ||||||
|  |     }); | ||||||
| 
 | 
 | ||||||
|  | /*global Promise*/ | ||||||
|  | var keypairs = require("keypairs"); | ||||||
|  | keypairs.generate().then(function (pair) { | ||||||
|  |     var iss = "https://example.com/"; | ||||||
|  |     return Promise.all([ | ||||||
|  |         keypairs | ||||||
|  |             .signJwt({ | ||||||
|  |                 jwk: pair.private, | ||||||
|  |                 iss: iss, | ||||||
|  |                 sub: "mikey", | ||||||
|  |                 exp: "1h" | ||||||
|  |             }) | ||||||
|  |             .then(function (jwt) { | ||||||
|  |                 return Promise.all([ | ||||||
|  |                     keyfetch.jwt.verify(jwt, { jwk: pair.public, iss: "*" }).then(function (verified) { | ||||||
|  |                         if (!(verified.claims && verified.claims.exp)) { | ||||||
|  |                             throw new Error("malformed decoded token"); | ||||||
|  |                         } | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(keyfetch.jwt.decode(jwt), { jwk: pair.public, iss: iss }) | ||||||
|  |                         .then(function (verified) { | ||||||
|  |                             if (!(verified.claims && verified.claims.exp)) { | ||||||
|  |                                 throw new Error("malformed decoded token"); | ||||||
|  |                             } | ||||||
|  |                         }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { jwks: [pair.public], issuers: [iss] }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         issuers: ["https://example.com/"] | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         issuers: ["https://example.com"] | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         issuers: ["example.com"] | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         issuers: ["example.com/"] | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         issuers: ["*"] | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             issuers: ["http://example.com"] | ||||||
|  |                         }) | ||||||
|  |                         .then(e("bad scheme")) | ||||||
|  |                         .catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             issuers: ["https://www.example.com"] | ||||||
|  |                         }) | ||||||
|  |                         .then(e("bad prefix")) | ||||||
|  |                         .catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             issuers: ["https://wexample.com"] | ||||||
|  |                         }) | ||||||
|  |                         .then(e("bad sld")) | ||||||
|  |                         .catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             issuers: ["https://example.comm"] | ||||||
|  |                         }) | ||||||
|  |                         .then(e("bad tld")) | ||||||
|  |                         .catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { | ||||||
|  |                         jwk: pair.public, | ||||||
|  |                         claims: { iss: "https://example.com/" } | ||||||
|  |                     }), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             claims: { iss: "https://example.com" } | ||||||
|  |                         }) | ||||||
|  |                         .then(e("inexact claim")) | ||||||
|  |                         .catch(throwIfNotExpected) | ||||||
|  |                 ]); | ||||||
|  |             }), | ||||||
|  |         keypairs | ||||||
|  |             .signJwt({ | ||||||
|  |                 jwk: pair.private, | ||||||
|  |                 iss: false, | ||||||
|  |                 sub: "mikey", | ||||||
|  |                 exp: "1h" | ||||||
|  |             }) | ||||||
|  |             .then(function (jwt) { | ||||||
|  |                 return Promise.all([ | ||||||
|  |                     // test that the old behavior of defaulting to '*' still works
 | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { jwk: pair.public }) | ||||||
|  |                         .then(e("should have issued security warning about allow all by default")) | ||||||
|  |                         .catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ["*"] }), | ||||||
|  |                     keyfetch.jwt.verify(jwt).then(e("should have an issuer")).catch(throwIfNotExpected), | ||||||
|  |                     keyfetch.jwt | ||||||
|  |                         .verify(jwt, { | ||||||
|  |                             jwk: pair.public, | ||||||
|  |                             issuers: ["https://example.com/"] | ||||||
|  |                         }) | ||||||
|  |                         .then(e("fail when issuer specified and doesn't exist")) | ||||||
|  |                         .catch(throwIfNotExpected) | ||||||
|  |                 ]); | ||||||
|  |             }) | ||||||
|  |     ]) | ||||||
|  |         .then(function () { | ||||||
|  |             console.log("JWT PASSES"); | ||||||
|  |         }) | ||||||
|  |         .catch(function (err) { | ||||||
|  |             console.error("NONE SHALL PASS!"); | ||||||
|  |             console.error(err); | ||||||
|  |             process.exit(1); | ||||||
|  |         }); | ||||||
|  | }); | ||||||
| /* | /* | ||||||
| var jwt = '...'; | var jwt = '...'; | ||||||
| keyfetch.verify({ jwt: jwt }).catch(function (err) { | keyfetch.verify({ jwt: jwt }).catch(function (err) { | ||||||
|   console.log(err); |   console.log(err); | ||||||
| }); | }); | ||||||
| */ | */ | ||||||
|  | 
 | ||||||
|  | function e(msg) { | ||||||
|  |     return new Error("ETEST: " + msg); | ||||||
|  | } | ||||||
|  | function throwIfNotExpected(err) { | ||||||
|  |     if ("ETEST" === err.message.slice(0, 5)) { | ||||||
|  |         throw err; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										587
									
								
								keyfetch.js
									
									
									
									
									
								
							
							
						
						
									
										587
									
								
								keyfetch.js
									
									
									
									
									
								
							| @ -1,158 +1,163 @@ | |||||||
| 'use strict'; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var keyfetch = module.exports; | var keyfetch = module.exports; | ||||||
| 
 | 
 | ||||||
| var promisify = require('util').promisify; | var request = require("@root/request").defaults({ | ||||||
| var requestAsync = promisify(require('@coolaj86/urequest')); |     userAgent: "keyfetch/v2.1.0" | ||||||
| var Rasha = require('rasha'); | }); | ||||||
| var Eckles = require('eckles'); | var Rasha = require("rasha"); | ||||||
|  | var Eckles = require("eckles"); | ||||||
| var mincache = 1 * 60 * 60; | var mincache = 1 * 60 * 60; | ||||||
| var maxcache = 3 * 24 * 60 * 60; | var maxcache = 3 * 24 * 60 * 60; | ||||||
| var staletime = 15 * 60; | var staletime = 15 * 60; | ||||||
| var keyCache = {}; | var keyCache = {}; | ||||||
| 
 | 
 | ||||||
| /*global Promise*/ | var Errors = require("./lib/errors.js"); | ||||||
| function checkMinDefaultMax(opts, key, n, d, x) { | 
 | ||||||
|   var i = opts[key]; | async function requestAsync(req) { | ||||||
|   if (!i && 0 !== i) { return d; } |     var resp = await request(req).catch(Errors.BAD_GATEWAY); | ||||||
|   if (i >= n && i >= x) { | 
 | ||||||
|     return parseInt(i, 10); |     // differentiate potentially temporary server errors from 404
 | ||||||
|   } else { |     if (!resp.ok && (resp.statusCode >= 500 || resp.statusCode < 200)) { | ||||||
|     throw new Error("opts." + key + " should be at least " + n + " and at most " + x + ", not " + i); |         throw Errors.BAD_GATEWAY({ response: resp }); | ||||||
|   } |     } | ||||||
|  | 
 | ||||||
|  |     return resp; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function checkMinDefaultMax(opts, key, n, d, x) { | ||||||
|  |     var i = opts[key]; | ||||||
|  |     if (!i && 0 !== i) { | ||||||
|  |         return d; | ||||||
|  |     } | ||||||
|  |     if (i >= n && i >= x) { | ||||||
|  |         return parseInt(i, 10); | ||||||
|  |     } else { | ||||||
|  |         throw Errors.DEVELOPER_ERROR("opts." + key + " should be at least " + n + " and at most " + x + ", not " + i); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | keyfetch._errors = Errors; | ||||||
|  | 
 | ||||||
|  | keyfetch._clear = function () { | ||||||
|  |     keyCache = {}; | ||||||
|  | }; | ||||||
| keyfetch.init = function (opts) { | keyfetch.init = function (opts) { | ||||||
|   mincache = checkMinDefaultMax(opts, 'mincache', |     mincache = checkMinDefaultMax(opts, "mincache", 1 * 60, mincache, 31 * 24 * 60 * 60); | ||||||
|     1 * 60, |     maxcache = checkMinDefaultMax(opts, "maxcache", 1 * 60 * 60, maxcache, 31 * 24 * 60 * 60); | ||||||
|     mincache, |     staletime = checkMinDefaultMax(opts, "staletime", 1 * 60, staletime, 31 * 24 * 60 * 60); | ||||||
|     31 * 24 * 60 * 60 |  | ||||||
|   ); |  | ||||||
|   maxcache = checkMinDefaultMax(opts, 'maxcache', |  | ||||||
|     1 * 60 * 60, |  | ||||||
|     maxcache, |  | ||||||
|     31 * 24 * 60 * 60 |  | ||||||
|   ); |  | ||||||
|   staletime = checkMinDefaultMax(opts, 'staletime', |  | ||||||
|     1 * 60, |  | ||||||
|     staletime, |  | ||||||
|     31 * 24 * 60 * 60 |  | ||||||
|   ); |  | ||||||
| }; | }; | ||||||
| keyfetch._oidc = function (iss) { | keyfetch._oidc = async function (iss) { | ||||||
|   return Promise.resolve().then(function () { |     var url = normalizeIss(iss) + "/.well-known/openid-configuration"; | ||||||
|     return requestAsync({ |     var resp = await requestAsync({ | ||||||
|       url: normalizeIss(iss) + '/.well-known/openid-configuration' |         url: url, | ||||||
|     , json: true |         json: true | ||||||
|     }).then(function (resp) { |  | ||||||
|       var oidcConf = resp.body; |  | ||||||
|       if (!oidcConf.jwks_uri) { |  | ||||||
|         throw new Error("Failed to retrieve openid configuration"); |  | ||||||
|       } |  | ||||||
|       return oidcConf; |  | ||||||
|     }); |     }); | ||||||
|   }); | 
 | ||||||
|  |     var oidcConf = resp.body; | ||||||
|  |     if (!oidcConf.jwks_uri) { | ||||||
|  |         throw Errors.NO_JWKS_URI(url); | ||||||
|  |     } | ||||||
|  |     return oidcConf; | ||||||
| }; | }; | ||||||
| keyfetch._wellKnownJwks = function (iss) { | keyfetch._wellKnownJwks = async function (iss) { | ||||||
|   return Promise.resolve().then(function () { |     return keyfetch._jwks(normalizeIss(iss) + "/.well-known/jwks.json"); | ||||||
|     return keyfetch._jwks(normalizeIss(iss) + '/.well-known/jwks.json'); |  | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| keyfetch._jwks = function (iss) { | keyfetch._jwks = async function (iss) { | ||||||
|   return requestAsync({ url: iss, json: true }).then(function (resp) { |     var resp = await requestAsync({ url: iss, json: true }); | ||||||
|     return Promise.all(resp.body.keys.map(function (jwk) { | 
 | ||||||
|       // EC keys have an x values, whereas RSA keys do not
 |     return Promise.all( | ||||||
|       var Keypairs = jwk.x ? Eckles : Rasha; |         resp.body.keys.map(async function (jwk) { | ||||||
|       return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) { |             // EC keys have an x values, whereas RSA keys do not
 | ||||||
|         return Keypairs.export({ jwk: jwk }).then(function (pem) { |             var Keypairs = jwk.x ? Eckles : Rasha; | ||||||
|           var cacheable = { |             var thumbprint = await Keypairs.thumbprint({ jwk: jwk }); | ||||||
|             jwk: jwk |             var pem = await Keypairs.export({ jwk: jwk }); | ||||||
|           , thumbprint: thumbprint |             var cacheable = { | ||||||
|           , pem: pem |                 jwk: jwk, | ||||||
|           }; |                 thumbprint: thumbprint, | ||||||
|           return cacheable; |                 pem: pem | ||||||
|         }); |             }; | ||||||
|       }); |             return cacheable; | ||||||
|     })); |         }) | ||||||
|   }); |     ); | ||||||
| }; | }; | ||||||
| keyfetch.jwks = function (jwkUrl) { | keyfetch.jwks = async function (jwkUrl) { | ||||||
|   // TODO DRY up a bit
 |  | ||||||
|   return keyfetch._jwks(jwkUrl).then(function (results) { |  | ||||||
|     return Promise.all(results.map(function (result) { |  | ||||||
|       return keyfetch._setCache(result.jwk.iss || jwkUrl, result); |  | ||||||
|     })).then(function () { |  | ||||||
|       // cacheable -> hit (keep original externally immutable)
 |  | ||||||
|       return JSON.parse(JSON.stringify(results)); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| keyfetch.wellKnownJwks = function (iss) { |  | ||||||
|   // TODO DRY up a bit
 |  | ||||||
|   return keyfetch._wellKnownJwks(iss).then(function (results) { |  | ||||||
|     return Promise.all(results.map(function (result) { |  | ||||||
|       return keyfetch._setCache(result.jwk.iss || iss, result); |  | ||||||
|     })).then(function () { |  | ||||||
|       // result -> hit (keep original externally immutable)
 |  | ||||||
|       return JSON.parse(JSON.stringify(results)); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| keyfetch.oidcJwks = function (iss) { |  | ||||||
|   return keyfetch._oidc(iss).then(function (oidcConf) { |  | ||||||
|     // TODO DRY up a bit
 |     // TODO DRY up a bit
 | ||||||
|     return keyfetch._jwks(oidcConf.jwks_uri).then(function (results) { |     var results = await keyfetch._jwks(jwkUrl); | ||||||
|       return Promise.all(results.map(function (result) { |     await Promise.all( | ||||||
|         return keyfetch._setCache(result.jwk.iss || iss, result); |         results.map(async function (result) { | ||||||
|       })).then(function () { |             return keyfetch._setCache(result.jwk.iss || jwkUrl, result); | ||||||
|         // result -> hit (keep original externally immutable)
 |         }) | ||||||
|         return JSON.parse(JSON.stringify(results)); |     ); | ||||||
|       }); |     // cacheable -> hit (keep original externally immutable)
 | ||||||
|     }); |     return JSON.parse(JSON.stringify(results)); | ||||||
|   }); | }; | ||||||
|  | keyfetch.wellKnownJwks = async function (iss) { | ||||||
|  |     // TODO DRY up a bit
 | ||||||
|  |     var results = await keyfetch._wellKnownJwks(iss); | ||||||
|  |     await Promise.all( | ||||||
|  |         results.map(async function (result) { | ||||||
|  |             return keyfetch._setCache(result.jwk.iss || iss, result); | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  |     // result -> hit (keep original externally immutable)
 | ||||||
|  |     return JSON.parse(JSON.stringify(results)); | ||||||
|  | }; | ||||||
|  | keyfetch.oidcJwks = async function (iss) { | ||||||
|  |     var oidcConf = await keyfetch._oidc(iss); | ||||||
|  |     // TODO DRY up a bit
 | ||||||
|  |     var results = await keyfetch._jwks(oidcConf.jwks_uri); | ||||||
|  |     await Promise.all( | ||||||
|  |         results.map(async function (result) { | ||||||
|  |             return keyfetch._setCache(result.jwk.iss || iss, result); | ||||||
|  |         }) | ||||||
|  |     ); | ||||||
|  |     // result -> hit (keep original externally immutable)
 | ||||||
|  |     return JSON.parse(JSON.stringify(results)); | ||||||
| }; | }; | ||||||
| function checkId(id) { | function checkId(id) { | ||||||
|   return function (results) { |     return function (results) { | ||||||
|     var result = results.some(function (result) { |         var result = results.filter(function (result) { | ||||||
|       // we already checked iss above
 |             // we already checked iss above
 | ||||||
|       console.log(result); |             return result.jwk.kid === id || result.thumbprint === id; | ||||||
|       return result.jwk.kid === id || result.thumbprint === id; |         })[0]; | ||||||
|     })[0]; |  | ||||||
| 
 | 
 | ||||||
|     if (!result) { |         if (!result) { | ||||||
|       throw new Error("No JWK found by kid or thumbprint '" + id + "'"); |             throw Errors.JWK_NOT_FOUND(id); | ||||||
|     } |         } | ||||||
|     return result; |         return result; | ||||||
|   }; |     }; | ||||||
| } | } | ||||||
| keyfetch.oidcJwk = function (id, iss) { | keyfetch.oidcJwk = async function (id, iss) { | ||||||
|   return keyfetch._checkCache(id, iss).then(function (hit) { |     var hit = await keyfetch._checkCache(id, iss); | ||||||
|     if (hit) { return hit; } |     if (hit) { | ||||||
|  |         return hit; | ||||||
|  |     } | ||||||
|     return keyfetch.oidcJwks(iss).then(checkId(id)); |     return keyfetch.oidcJwks(iss).then(checkId(id)); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| keyfetch.wellKnownJwk = function (id, iss) { | keyfetch.wellKnownJwk = async function (id, iss) { | ||||||
|   return keyfetch._checkCache(id, iss).then(function (hit) { |     var hit = await keyfetch._checkCache(id, iss); | ||||||
|     if (hit) { return hit; } |     if (hit) { | ||||||
|  |         return hit; | ||||||
|  |     } | ||||||
|     return keyfetch.wellKnownJwks(iss).then(checkId(id)); |     return keyfetch.wellKnownJwks(iss).then(checkId(id)); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| keyfetch.jwk = function (id, jwksUrl) { | keyfetch.jwk = async function (id, jwksUrl) { | ||||||
|   return keyfetch._checkCache(id, jwksUrl).then(function (hit) { |     var hit = await keyfetch._checkCache(id, jwksUrl); | ||||||
|     if (hit) { return hit; } |     if (hit) { | ||||||
|  |         return hit; | ||||||
|  |     } | ||||||
|     return keyfetch.jwks(jwksUrl).then(checkId(id)); |     return keyfetch.jwks(jwksUrl).then(checkId(id)); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| keyfetch._checkCache = function (id, iss) { | keyfetch._checkCache = async function (id, iss) { | ||||||
|   return Promise.resolve().then(function () { |  | ||||||
|     // We cache by thumbprint and (kid + '@' + iss),
 |     // We cache by thumbprint and (kid + '@' + iss),
 | ||||||
|     // so it's safe to check without appending the issuer
 |     // so it's safe to check without appending the issuer
 | ||||||
|     var hit = keyCache[id]; |     var hit = keyCache[id]; | ||||||
|     if (!hit) { |     if (!hit) { | ||||||
|       hit = keyCache[id + '@' + normalizeIss(iss)]; |         hit = keyCache[id + "@" + normalizeIss(iss)]; | ||||||
|     } |     } | ||||||
|     if (!hit) { |     if (!hit) { | ||||||
|       return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var now = Math.round(Date.now() / 1000); |     var now = Math.round(Date.now() / 1000); | ||||||
| @ -160,114 +165,282 @@ keyfetch._checkCache = function (id, iss) { | |||||||
|     // not guarding number checks since we know that we
 |     // not guarding number checks since we know that we
 | ||||||
|     // set 'now' and 'expiresAt' correctly elsewhere
 |     // set 'now' and 'expiresAt' correctly elsewhere
 | ||||||
|     if (left > staletime) { |     if (left > staletime) { | ||||||
|       return JSON.parse(JSON.stringify(hit)); |         return JSON.parse(JSON.stringify(hit)); | ||||||
|     } |     } | ||||||
|     if (left > 0) { |     if (left > 0) { | ||||||
|       return JSON.parse(JSON.stringify(hit)); |         return JSON.parse(JSON.stringify(hit)); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| keyfetch._setCache = function (iss, cacheable) { | keyfetch._setCache = function (iss, cacheable) { | ||||||
|   // force into a number
 |     // force into a number
 | ||||||
|   var expiresAt = parseInt(cacheable.jwk.exp, 10) || 0; |     var expiresAt = parseInt(cacheable.jwk.exp, 10) || 0; | ||||||
|   var now = Date.now() / 1000; |     var now = Date.now() / 1000; | ||||||
|   var left = expiresAt - now; |     var left = expiresAt - now; | ||||||
| 
 | 
 | ||||||
|   // TODO maybe log out when any of these non-ideal cases happen?
 |     // TODO maybe log out when any of these non-ideal cases happen?
 | ||||||
|   if (!left) { |     if (!left) { | ||||||
|     expiresAt = now + maxcache; |         expiresAt = now + maxcache; | ||||||
|   } else if (left < mincache) { |     } else if (left < mincache) { | ||||||
|     expiresAt = now + mincache; |         expiresAt = now + mincache; | ||||||
|   } else if (left > maxcache) { |     } else if (left > maxcache) { | ||||||
|     expiresAt = now + maxcache; |         expiresAt = now + maxcache; | ||||||
|   } |     } | ||||||
| 
 | 
 | ||||||
|   // cacheable = { jwk, thumprint, pem }
 |     // cacheable = { jwk, thumprint, pem }
 | ||||||
|   cacheable.createdAt = now; |     cacheable.createdAt = now; | ||||||
|   cacheable.expiresAt = expiresAt; |     cacheable.expiresAt = expiresAt; | ||||||
|   keyCache[cacheable.thumbprint] = cacheable; |     keyCache[cacheable.thumbprint] = cacheable; | ||||||
|   keyCache[cacheable.jwk.kid + '@' + normalizeIss(iss)] = cacheable; |     keyCache[cacheable.jwk.kid + "@" + normalizeIss(iss)] = cacheable; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function normalizeIss(iss) { | function normalizeIss(iss) { | ||||||
|   // We definitely don't want false negatives stemming
 |     if (!iss) { | ||||||
|   // from https://example.com vs https://example.com/
 |         throw Errors.NO_ISSUER(); | ||||||
|   // We also don't want to allow insecure issuers
 |     } | ||||||
|   if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) { | 
 | ||||||
|     // note, we wrap some things in promises just so we can throw here
 |     // We definitely don't want false negatives stemming
 | ||||||
|     throw new Error("'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing."); |     // from https://example.com vs https://example.com/
 | ||||||
|   } |     // We also don't want to allow insecure issuers
 | ||||||
|   return iss.replace(/\/$/, ''); |     if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) { | ||||||
|  |         // note, we wrap some things in promises just so we can throw here
 | ||||||
|  |         throw Errors.INSECURE_ISSUER(iss); | ||||||
|  |     } | ||||||
|  |     return iss.replace(/\/$/, ""); | ||||||
| } | } | ||||||
| keyfetch._decode = function (jwt) { | 
 | ||||||
|   var parts = jwt.split('.'); | keyfetch.jwt = {}; | ||||||
|   return { | keyfetch.jwt.decode = function (jwt) { | ||||||
|     header: JSON.parse(Buffer.from(parts[0], 'base64')) |     try { | ||||||
|   , payload: JSON.parse(Buffer.from(parts[1], 'base64')) |         var parts = jwt.split("."); | ||||||
|   , signature: parts[2] //Buffer.from(parts[2], 'base64')
 |         // JWS
 | ||||||
|   }; |         var obj = { protected: parts[0], payload: parts[1], signature: parts[2] }; | ||||||
|  |         // JWT
 | ||||||
|  |         obj.header = JSON.parse(Buffer.from(obj.protected, "base64")); | ||||||
|  |         obj.claims = JSON.parse(Buffer.from(obj.payload, "base64")); | ||||||
|  |         return obj; | ||||||
|  |     } catch (e) { | ||||||
|  |         var err = Errors.PARSE_ERROR(jwt); | ||||||
|  |         err.details = e.message; | ||||||
|  |         throw err; | ||||||
|  |     } | ||||||
| }; | }; | ||||||
| keyfetch.verify = function (opts) { | keyfetch.jwt.verify = async function (jwt, opts) { | ||||||
|   var jwt = opts.jwt; |     if (!opts) { | ||||||
|   return Promise.resolve().then(function () { |         opts = {}; | ||||||
|     var decoded; |     } | ||||||
|  | 
 | ||||||
|  |     var jws; | ||||||
|     var exp; |     var exp; | ||||||
|     var nbf; |     var nbf; | ||||||
|     var valid; |     var active; | ||||||
|     try { |     var now; | ||||||
|       decoded = keyfetch._decode(jwt); |     var then; | ||||||
|       exp = decoded.payload.exp; |     var issuers = opts.issuers || []; | ||||||
|       nbf = decoded.payload.nbf; |     if (opts.iss) { | ||||||
|     } catch (e) { |         issuers.push(opts.iss); | ||||||
|       throw new Error("could not parse opts.jwt: '" + jwt + "'"); |  | ||||||
|     } |     } | ||||||
|     if (exp) { |     if (opts.claims && opts.claims.iss) { | ||||||
|       valid = (parseInt(exp, 10) - (Date.now()/1000) > 0); |         issuers.push(opts.claims.iss); | ||||||
|       if (!valid) { |  | ||||||
|         throw new Error("token's 'exp' has passed or could not parsed: '" + exp + "'"); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |     if (!issuers.length) { | ||||||
|  |         if (!(opts.jwk || opts.jwks)) { | ||||||
|  |             throw Errors.DEVELOPER_ERROR( | ||||||
|  |                 "[keyfetch.js] Security Error: Neither of opts.issuers nor opts.iss were provided. If you would like to bypass issuer verification (i.e. for federated authn) you must explicitly set opts.issuers = ['*']. Otherwise set a value such as https://accounts.google.com/" | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     var claims = opts.claims || {}; | ||||||
|  |     if (!jwt || "string" === typeof jwt) { | ||||||
|  |         jws = keyfetch.jwt.decode(jwt); | ||||||
|  |     } else { | ||||||
|  |         jws = jwt; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!jws.claims.iss || !issuers.some(isTrustedIssuer(jws.claims.iss))) { | ||||||
|  |         if (!(opts.jwk || opts.jwks)) { | ||||||
|  |             throw Errors.UNKNOWN_ISSUER(jws.claims.iss || ""); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     // Note claims.iss validates more strictly than opts.issuers (requires exact match)
 | ||||||
|  |     var failedClaims = Object.keys(claims) | ||||||
|  |         .filter(function (key) { | ||||||
|  |             if (claims[key] !== jws.claims[key]) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         .map(function (key) { | ||||||
|  |             return "jwt.claims." + key + " = " + JSON.stringify(jws.claims[key]); | ||||||
|  |         }); | ||||||
|  |     if (failedClaims.length) { | ||||||
|  |         throw Errors.FAILED_CLAIMS(failedClaims, Object.keys(claims)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     exp = jws.claims.exp; | ||||||
|  |     if (exp && false !== opts.exp) { | ||||||
|  |         now = Date.now(); | ||||||
|  |         // TODO document that opts.exp can be used as leeway? Or introduce opts.leeway?
 | ||||||
|  |         // fair, but not necessary
 | ||||||
|  |         exp = parseInt(exp, 10); | ||||||
|  |         if (isNaN(exp)) { | ||||||
|  |             throw Errors.MALFORMED_EXP(JSON.stringify(jws.claims.exp)); | ||||||
|  |         } | ||||||
|  |         then = (opts.exp || 0) + parseInt(exp, 10); | ||||||
|  |         active = then - now / 1000 > 0; | ||||||
|  |         // expiration was on the token or, if not, such a token is not allowed
 | ||||||
|  |         if (!active) { | ||||||
|  |             throw Errors.EXPIRED(exp); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     nbf = jws.claims.nbf; | ||||||
|     if (nbf) { |     if (nbf) { | ||||||
|       valid = (parseInt(nbf, 10) - (Date.now()/1000) <= 0); |         active = parseInt(nbf, 10) - Date.now() / 1000 <= 0; | ||||||
|       if (!valid) { |         if (!active) { | ||||||
|         throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'"); |             throw Errors.INACTIVE(nbf); | ||||||
|       } |         } | ||||||
|     } |     } | ||||||
|     var kid = decoded.header.kid; |     if (opts.jwks || opts.jwk) { | ||||||
|  |         return overrideLookup(opts.jwks || [opts.jwk]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var kid = jws.header.kid; | ||||||
|     var iss; |     var iss; | ||||||
|     var fetcher; |     var fetcher; | ||||||
|     if (!opts.strategy || 'oidc' === opts.strategy) { |     var fetchOne; | ||||||
|       iss = decoded.payload.iss; |     if (!opts.strategy || "oidc" === opts.strategy) { | ||||||
|       fetcher = keyfetch.oidcJwks; |         iss = jws.claims.iss; | ||||||
|     } else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) { |         fetcher = keyfetch.oidcJwks; | ||||||
|       iss = decoded.payload.iss; |         fetchOne = keyfetch.oidcJwk; | ||||||
|       fetcher = keyfetch.wellKnownJwks; |     } else if ("auth0" === opts.strategy || "well-known" === opts.strategy) { | ||||||
|  |         iss = jws.claims.iss; | ||||||
|  |         fetcher = keyfetch.wellKnownJwks; | ||||||
|  |         fetchOne = keyfetch.wellKnownJwk; | ||||||
|     } else { |     } else { | ||||||
|       iss = opts.strategy; |         iss = opts.strategy; | ||||||
|       fetcher = keyfetch.jwks; |         fetcher = keyfetch.jwks; | ||||||
|  |         fetchOne = keyfetch.jwk; | ||||||
|     } |     } | ||||||
|     function verify(jwk, payload) { | 
 | ||||||
|       var alg = 'RSA-SHA' + decoded.header.alg.replace(/[^\d]+/i, ''); |     if (kid) { | ||||||
|       return require('crypto') |         return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
 | ||||||
|         .createVerify(alg) |  | ||||||
|         .update(jwt.split('.')[0] + '.' + payload) |  | ||||||
|         .verify(jwk.pem, decoded.signature, 'base64'); |  | ||||||
|     } |     } | ||||||
|     return fetcher(iss).then(function (jwks) { |     return fetcher(iss).then(verifyAny); | ||||||
|       var payload = jwt.split('.')[1]; // as string, as it was signed
 | 
 | ||||||
|       if (jwks.some(function (jwk) { |     function verifyOne(hit) { | ||||||
|         if (kid) { |         if (true === keyfetch.jws.verify(jws, hit)) { | ||||||
|           if (kid !== jwk.kid && kid !== jwk.thumbprint) { return; } |             return jws; | ||||||
|           if (verify(jwk, payload)) { return true; } |  | ||||||
|           throw new Error('token signature verification was unsuccessful'); |  | ||||||
|         } else { |  | ||||||
|           if (verify(jwk, payload)) { return true; } |  | ||||||
|         } |         } | ||||||
|       })) { |         throw Errors.BAD_SIGNATURE(jws.protected + "." + jws.payload + "." + jws.signature); | ||||||
|         return decoded; |     } | ||||||
|       } | 
 | ||||||
|       throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); |     function verifyAny(hits) { | ||||||
|     }); |         if ( | ||||||
|   }); |             hits.some(function (hit) { | ||||||
|  |                 if (kid) { | ||||||
|  |                     if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     if (true === keyfetch.jws.verify(jws, hit)) { | ||||||
|  |                         return true; | ||||||
|  |                     } | ||||||
|  |                     throw Errors.BAD_SIGNATURE(); | ||||||
|  |                 } | ||||||
|  |                 if (true === keyfetch.jws.verify(jws, hit)) { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |         ) { | ||||||
|  |             return jws; | ||||||
|  |         } | ||||||
|  |         throw Errors.JWK_NOT_FOUND_OLD(kid); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function overrideLookup(jwks) { | ||||||
|  |         return Promise.all( | ||||||
|  |             jwks.map(async function (jwk) { | ||||||
|  |                 var Keypairs = jwk.x ? Eckles : Rasha; | ||||||
|  |                 var pem = await Keypairs.export({ jwk: jwk }); | ||||||
|  |                 var thumb = await Keypairs.thumbprint({ jwk: jwk }); | ||||||
|  |                 return { jwk: jwk, pem: pem, thumbprint: thumb }; | ||||||
|  |             }) | ||||||
|  |         ).then(verifyAny); | ||||||
|  |     } | ||||||
| }; | }; | ||||||
|  | keyfetch.jws = {}; | ||||||
|  | keyfetch.jws.verify = function (jws, pub) { | ||||||
|  |     var alg = "SHA" + jws.header.alg.replace(/[^\d]+/i, ""); | ||||||
|  |     var sig = ecdsaJoseSigToAsn1Sig(jws.header, jws.signature); | ||||||
|  |     return require("crypto") | ||||||
|  |         .createVerify(alg) | ||||||
|  |         .update(jws.protected + "." + jws.payload) | ||||||
|  |         .verify(pub.pem, sig, "base64"); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // old, gotta make sure nothing else uses this
 | ||||||
|  | keyfetch._decode = function (jwt) { | ||||||
|  |     var obj = keyfetch.jwt.decode(jwt); | ||||||
|  |     return { header: obj.header, payload: obj.claims, signature: obj.signature }; | ||||||
|  | }; | ||||||
|  | keyfetch.verify = async function (opts) { | ||||||
|  |     var jwt = opts.jwt; | ||||||
|  |     var obj = await keyfetch.jwt.verify(jwt, opts); | ||||||
|  |     return { header: obj.header, payload: obj.claims, signature: obj.signature }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function ecdsaJoseSigToAsn1Sig(header, b64sig) { | ||||||
|  |     // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||||
|  |     // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||||
|  |     if (!/^ES/i.test(header.alg)) { | ||||||
|  |         return b64sig; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var bufsig = Buffer.from(b64sig, "base64"); | ||||||
|  |     var hlen = bufsig.byteLength / 2; // should be even
 | ||||||
|  |     var r = bufsig.slice(0, hlen); | ||||||
|  |     var s = bufsig.slice(hlen); | ||||||
|  |     // unpad positive ints less than 32 bytes wide
 | ||||||
|  |     while (!r[0]) { | ||||||
|  |         r = r.slice(1); | ||||||
|  |     } | ||||||
|  |     while (!s[0]) { | ||||||
|  |         s = s.slice(1); | ||||||
|  |     } | ||||||
|  |     // pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide
 | ||||||
|  |     if (0x80 & r[0]) { | ||||||
|  |         r = Buffer.concat([Buffer.from([0]), r]); | ||||||
|  |     } | ||||||
|  |     if (0x80 & s[0]) { | ||||||
|  |         s = Buffer.concat([Buffer.from([0]), s]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var len = 2 + r.byteLength + 2 + s.byteLength; | ||||||
|  |     var head = [0x30]; | ||||||
|  |     // hard code 0x80 + 1 because it won't be longer than
 | ||||||
|  |     // two SHA512 plus two pad bytes (130 bytes <= 256)
 | ||||||
|  |     if (len >= 0x80) { | ||||||
|  |         head.push(0x81); | ||||||
|  |     } | ||||||
|  |     head.push(len); | ||||||
|  | 
 | ||||||
|  |     var buf = Buffer.concat([ | ||||||
|  |         Buffer.from(head), | ||||||
|  |         Buffer.from([0x02, r.byteLength]), | ||||||
|  |         r, | ||||||
|  |         Buffer.from([0x02, s.byteLength]), | ||||||
|  |         s | ||||||
|  |     ]); | ||||||
|  | 
 | ||||||
|  |     return buf.toString("base64").replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, ""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isTrustedIssuer(issuer) { | ||||||
|  |     return function (trusted) { | ||||||
|  |         if ("*" === trusted) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         // TODO account for '*.example.com'
 | ||||||
|  |         trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted; | ||||||
|  |         return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted; | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										270
									
								
								lib/errors.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								lib/errors.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,270 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | // Possible User Errors
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @typedef AuthError | ||||||
|  |  * @property {string} message | ||||||
|  |  * @property {number} status | ||||||
|  |  * @property {string} code | ||||||
|  |  * @property {any} [details] | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @param {string} msg | ||||||
|  |  * @param {{ | ||||||
|  |  *   status: number, | ||||||
|  |  *   code: string, | ||||||
|  |  *   details?: any, | ||||||
|  |  * }} opts | ||||||
|  |  * @returns {AuthError} | ||||||
|  |  */ | ||||||
|  | function create(old, msg, code, status, details) { | ||||||
|  |     /** @type AuthError */ | ||||||
|  |     //@ts-ignore
 | ||||||
|  |     let err = new Error(msg); | ||||||
|  |     err.message = msg; | ||||||
|  |     err._old_message = old; | ||||||
|  |     err.code = code; | ||||||
|  |     err.status = status; | ||||||
|  |     if (details) { | ||||||
|  |         err.details = details; | ||||||
|  |     } | ||||||
|  |     err.source = "keyfetch"; | ||||||
|  |     err.toJSON = toJSON; | ||||||
|  |     err.toString = toString; | ||||||
|  |     return err; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function toJSON() { | ||||||
|  |     /*jshint validthis:true*/ | ||||||
|  |     return { | ||||||
|  |         message: this.message, | ||||||
|  |         status: this.status, | ||||||
|  |         code: this.code, | ||||||
|  |         details: this.details | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  | function toString() { | ||||||
|  |     /*jshint validthis:true*/ | ||||||
|  |     return this.stack + "\n" + JSON.stringify(this); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DEVELOPER_ERROR - a good token won't make a difference
 | ||||||
|  | var E_DEVELOPER = "DEVELOPER_ERROR"; | ||||||
|  | 
 | ||||||
|  | // BAD_GATEWAY - there may be a temporary error fetching the public or or whatever
 | ||||||
|  | var E_BAD_GATEWAY = "BAD_GATEWAY"; | ||||||
|  | 
 | ||||||
|  | // MALFORMED_JWT - the token could not be verified - not parsable, missing claims, etc
 | ||||||
|  | var E_MALFORMED = "MALFORMED_JWT"; | ||||||
|  | 
 | ||||||
|  | // INVALID_JWT - the token's properties don't meet requirements - iss, claims, sig, exp
 | ||||||
|  | var E_INVALID = "INVALID_JWT"; | ||||||
|  | 
 | ||||||
|  | module.exports = { | ||||||
|  |     //
 | ||||||
|  |     // DEVELOPER_ERROR (dev / server)
 | ||||||
|  |     //
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param {string} msg | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     DEVELOPER_ERROR: function (old, msg, details) { | ||||||
|  |         return create(old, msg || old, E_DEVELOPER, 500, details); | ||||||
|  |     }, | ||||||
|  |     BAD_GATEWAY: function (err) { | ||||||
|  |         var msg = | ||||||
|  |             "The auth token could not be verified because our server encountered a network error (or a bad gateway) when connecting to its issuing server."; | ||||||
|  |         var details = []; | ||||||
|  |         if (err.message) { | ||||||
|  |             details.push("error.message = " + err.message); | ||||||
|  |         } | ||||||
|  |         if (err.response && err.response.statusCode) { | ||||||
|  |             details.push("response.statusCode = " + err.response.statusCode); | ||||||
|  |         } | ||||||
|  |         return create(msg, msg, E_BAD_GATEWAY, 502, details); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     //
 | ||||||
|  |     // MALFORMED_TOKEN (dev / client)
 | ||||||
|  |     //
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param {string} iss | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     INSECURE_ISSUER: function (iss) { | ||||||
|  |         var old = | ||||||
|  |             "'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing. (iss)"; | ||||||
|  |         var details = [ | ||||||
|  |             "jwt.claims.iss = " + JSON.stringify(iss), | ||||||
|  |             "DEBUG: Set ENV 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow insecure issuers (for testing)." | ||||||
|  |         ]; | ||||||
|  |         var msg = | ||||||
|  |             'The auth token could not be verified because our server could connect to its issuing server ("iss") securely.'; | ||||||
|  |         return create(old, msg, E_MALFORMED, 400, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {string} jwt | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     PARSE_ERROR: function (jwt) { | ||||||
|  |         var old = "could not parse jwt: '" + jwt + "'"; | ||||||
|  |         var msg = "The auth token could not be verified because it is malformed."; | ||||||
|  |         var details = ["jwt = " + JSON.stringify(jwt)]; | ||||||
|  |         return create(old, msg, E_MALFORMED, 400, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {string} iss | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     NO_ISSUER: function (iss) { | ||||||
|  |         var old = "'iss' is not defined"; | ||||||
|  |         var msg = 'The auth token could not be verified because it doesn\'t specify an issuer ("iss").'; | ||||||
|  |         var details = ["jwt.claims.iss = " + JSON.stringify(iss)]; | ||||||
|  |         return create(old, msg, E_MALFORMED, 400, details); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param {string} iss | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     MALFORMED_EXP: function (exp) { | ||||||
|  |         var old = "token's 'exp' has passed or could not parsed: '" + exp + "'"; | ||||||
|  |         var msg = 'The auth token could not be verified because it\'s expiration date ("exp") could not be read'; | ||||||
|  |         var details = ["jwt.claims.exp = " + JSON.stringify(exp)]; | ||||||
|  |         return create(old, msg, E_MALFORMED, 400, details); | ||||||
|  |     }, | ||||||
|  | 
 | ||||||
|  |     //
 | ||||||
|  |     // INVALID_TOKEN (dev / client)
 | ||||||
|  |     //
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param {number} exp | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     EXPIRED: function (exp) { | ||||||
|  |         var old = "token's 'exp' has passed or could not parsed: '" + exp + "'"; | ||||||
|  |         // var msg = "The auth token did not pass verification because it is expired.not properly signed.";
 | ||||||
|  |         var msg = "The auth token is expired. To try again, go to the main page and sign in."; | ||||||
|  |         var details = ["jwt.claims.exp = " + JSON.stringify(exp)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {number} nbf | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     INACTIVE: function (nbf) { | ||||||
|  |         var old = "token's 'nbf' has not been reached or could not parsed: '" + nbf + "'"; | ||||||
|  |         var msg = "The auth token isn't valid yet. It's activation date (\"nbf\") is in the future."; | ||||||
|  |         var details = ["jwt.claims.nbf = " + JSON.stringify(nbf)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** @returns {AuthError} */ | ||||||
|  |     BAD_SIGNATURE: function (jwt) { | ||||||
|  |         var old = "token signature verification was unsuccessful"; | ||||||
|  |         var msg = "The auth token did not pass verification because it is not properly signed."; | ||||||
|  |         var details = ["jwt = " + JSON.stringify(jwt)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {string} kid | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     JWK_NOT_FOUND_OLD: function (kid) { | ||||||
|  |         var old = "Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."; | ||||||
|  |         var msg = | ||||||
|  |             'The auth token did not pass verification because our server couldn\'t find a mutually trusted verification key ("jwk").'; | ||||||
|  |         var details = ["jws.header.kid = " + JSON.stringify(kid)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {string} id | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     JWK_NOT_FOUND: function (id) { | ||||||
|  |         // TODO Distinguish between when it's a kid vs thumbprint.
 | ||||||
|  |         var old = "No JWK found by kid or thumbprint '" + id + "'"; | ||||||
|  |         var msg = | ||||||
|  |             'The auth token did not pass verification because our server couldn\'t find a mutually trusted verification key ("jwk").'; | ||||||
|  |         var details = ["jws.header.kid = " + JSON.stringify(id)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** @returns {AuthError} */ | ||||||
|  |     NO_JWKWS_URI: function (url) { | ||||||
|  |         var old = "Failed to retrieve openid configuration"; | ||||||
|  |         var msg = | ||||||
|  |             'The auth token did not pass verification because its issuing server did not list any verification keys ("jwks").'; | ||||||
|  |         var details = ["OpenID Provider Configuration: " + JSON.stringify(url)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {string} iss | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     UNKNOWN_ISSUER: function (iss) { | ||||||
|  |         var old = "token was issued by an untrusted issuer: '" + iss + "'"; | ||||||
|  |         var msg = "The auth token did not pass verification because it wasn't issued by a server that we trust."; | ||||||
|  |         var details = ["jwt.claims.iss = " + JSON.stringify(iss)]; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     }, | ||||||
|  |     /** | ||||||
|  |      * @param {Array<string>} details | ||||||
|  |      * @returns {AuthError} | ||||||
|  |      */ | ||||||
|  |     FAILED_CLAIMS: function (details, claimNames) { | ||||||
|  |         var old = "token did not match on one or more authorization claims: '" + claimNames + "'"; | ||||||
|  |         var msg = | ||||||
|  |             'The auth token did not pass verification because it failed some of the verification criteria ("claims").'; | ||||||
|  |         return create(old, msg, E_INVALID, 401, details); | ||||||
|  |     } | ||||||
|  | }; | ||||||
|  | var Errors = module.exports; | ||||||
|  | 
 | ||||||
|  | // for README
 | ||||||
|  | if (require.main === module) { | ||||||
|  |     let maxWidth = 54; | ||||||
|  |     let header = ["Hint", "Code", "Status", "Message (truncated)"]; | ||||||
|  |     let widths = header.map(function (v) { | ||||||
|  |         return Math.min(maxWidth, String(v).length); | ||||||
|  |     }); | ||||||
|  |     let rows = []; | ||||||
|  |     Object.keys(module.exports).forEach(function (k) { | ||||||
|  |         //@ts-ignore
 | ||||||
|  |         var E = module.exports[k]; | ||||||
|  |         var e = E("test"); | ||||||
|  |         var code = e.code; | ||||||
|  |         var msg = e.message; | ||||||
|  |         var hint = k.toLowerCase().replace(/_/g, " "); | ||||||
|  |         widths[0] = Math.max(widths[0], String(hint).length); | ||||||
|  |         widths[1] = Math.max(widths[1], String(code).length); | ||||||
|  |         widths[2] = Math.max(widths[2], String(e.status).length); | ||||||
|  |         widths[3] = Math.min(maxWidth, Math.max(widths[3], String(msg).length)); | ||||||
|  |         rows.push([hint, code, e.status, msg]); | ||||||
|  |     }); | ||||||
|  |     rows.forEach(function (cols, i) { | ||||||
|  |         let cells = cols.map(function (col, i) { | ||||||
|  |             if (col.length > maxWidth) { | ||||||
|  |                 col = col.slice(0, maxWidth - 3); | ||||||
|  |                 col += "..."; | ||||||
|  |             } | ||||||
|  |             return String(col).padEnd(widths[i], " "); | ||||||
|  |         }); | ||||||
|  |         let out = `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[3].slice(0, widths[3])} |`; | ||||||
|  |         //out = out.replace(/\| /g, " ").replace(/\|/g, "");
 | ||||||
|  |         console.info(out); | ||||||
|  |         if (i === 0) { | ||||||
|  |             cells = cols.map(function (col, i) { | ||||||
|  |                 return "-".padEnd(widths[i], "-"); | ||||||
|  |             }); | ||||||
|  |             console.info(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[3]} |`); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |     console.log(); | ||||||
|  |     console.log(Errors.MALFORMED_EXP()); | ||||||
|  |     console.log(); | ||||||
|  |     console.log(JSON.stringify(Errors.MALFORMED_EXP(), null, 2)); | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										97
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,23 +1,82 @@ | |||||||
| { | { | ||||||
|   "name": "keyfetch", |     "name": "keyfetch", | ||||||
|   "version": "1.1.0", |     "version": "3.0.2", | ||||||
|   "lockfileVersion": 1, |     "lockfileVersion": 2, | ||||||
|   "requires": true, |     "requires": true, | ||||||
|   "dependencies": { |     "packages": { | ||||||
|     "@coolaj86/urequest": { |         "": { | ||||||
|       "version": "1.3.6", |             "name": "keyfetch", | ||||||
|       "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", |             "version": "3.0.2", | ||||||
|       "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" |             "license": "MPL-2.0", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "@root/request": "^1.8.0", | ||||||
|  |                 "eckles": "^1.4.1", | ||||||
|  |                 "rasha": "^1.2.4" | ||||||
|  |             }, | ||||||
|  |             "devDependencies": { | ||||||
|  |                 "keypairs": "^1.2.14" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/@root/request": { | ||||||
|  |             "version": "1.8.0", | ||||||
|  |             "resolved": "https://registry.npmjs.org/@root/request/-/request-1.8.0.tgz", | ||||||
|  |             "integrity": "sha512-HufCvoTwqR30OyKSjwg28W5QCUpypSJZpOYcJbC9PME5kI6cOYsccYs/6bXfsuEoarz8+YwBDrsuM1UdBMxMLw==" | ||||||
|  |         }, | ||||||
|  |         "node_modules/eckles": { | ||||||
|  |             "version": "1.4.1", | ||||||
|  |             "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", | ||||||
|  |             "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==", | ||||||
|  |             "bin": { | ||||||
|  |                 "eckles": "bin/eckles.js" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/keypairs": { | ||||||
|  |             "version": "1.2.14", | ||||||
|  |             "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", | ||||||
|  |             "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", | ||||||
|  |             "dev": true, | ||||||
|  |             "dependencies": { | ||||||
|  |                 "eckles": "^1.4.1", | ||||||
|  |                 "rasha": "^1.2.4" | ||||||
|  |             }, | ||||||
|  |             "bin": { | ||||||
|  |                 "keypairs-install": "bin/keypairs.js" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "node_modules/rasha": { | ||||||
|  |             "version": "1.2.4", | ||||||
|  |             "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz", | ||||||
|  |             "integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q==", | ||||||
|  |             "bin": { | ||||||
|  |                 "rasha": "bin/rasha.js" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     }, |     }, | ||||||
|     "eckles": { |     "dependencies": { | ||||||
|       "version": "1.4.0", |         "@root/request": { | ||||||
|       "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz", |             "version": "1.8.0", | ||||||
|       "integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA==" |             "resolved": "https://registry.npmjs.org/@root/request/-/request-1.8.0.tgz", | ||||||
|     }, |             "integrity": "sha512-HufCvoTwqR30OyKSjwg28W5QCUpypSJZpOYcJbC9PME5kI6cOYsccYs/6bXfsuEoarz8+YwBDrsuM1UdBMxMLw==" | ||||||
|     "rasha": { |         }, | ||||||
|       "version": "1.2.1", |         "eckles": { | ||||||
|       "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz", |             "version": "1.4.1", | ||||||
|       "integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag==" |             "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", | ||||||
|  |             "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" | ||||||
|  |         }, | ||||||
|  |         "keypairs": { | ||||||
|  |             "version": "1.2.14", | ||||||
|  |             "resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", | ||||||
|  |             "integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", | ||||||
|  |             "dev": true, | ||||||
|  |             "requires": { | ||||||
|  |                 "eckles": "^1.4.1", | ||||||
|  |                 "rasha": "^1.2.4" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         "rasha": { | ||||||
|  |             "version": "1.2.4", | ||||||
|  |             "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz", | ||||||
|  |             "integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q==" | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|   } |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										72
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								package.json
									
									
									
									
									
								
							| @ -1,35 +1,39 @@ | |||||||
| { "author": { | { | ||||||
|     "name": "AJ ONeal", |     "name": "keyfetch", | ||||||
|     "email": "solderjs@gmail.com" |     "version": "3.0.2", | ||||||
|   }, |     "description": "Lightweight support for fetching JWKs.", | ||||||
|   "bundleDependencies": false, |     "homepage": "https://git.rootprojects.org/root/keyfetch.js", | ||||||
|   "dependencies": { |     "main": "keyfetch.js", | ||||||
|     "@coolaj86/urequest": "^1.3.6", |     "files": [ | ||||||
|     "eckles": "^1.4.0", |         "lib" | ||||||
|     "rasha": "^1.2.1" |     ], | ||||||
|   }, |     "dependencies": { | ||||||
|   "deprecated": false, |         "@root/request": "^1.8.0", | ||||||
|   "description": "Lightweight support for fetching JWKs.", |         "eckles": "^1.4.1", | ||||||
|   "files": [ |         "rasha": "^1.2.4" | ||||||
|     "keyfetch-test.js" |     }, | ||||||
|   ], |     "devDependencies": { | ||||||
|   "keywords": [ |         "keypairs": "^1.2.14" | ||||||
|     "jwks", |     }, | ||||||
|     "jwk", |     "scripts": { | ||||||
|     "jwt", |         "test": "node keyfetch-test.js" | ||||||
|     "auth0", |     }, | ||||||
|     "pem", |     "repository": { | ||||||
|     "RSA", |         "type": "git", | ||||||
|     "EC", |         "url": "https://git.rootprojects.org/root/keyfetch.js.git" | ||||||
|     "ECDSA", |     }, | ||||||
|     "OIDC", |     "keywords": [ | ||||||
|     "well-known" |         "jwks", | ||||||
|   ], |         "jwk", | ||||||
|   "license": "MPL-2.0", |         "jwt", | ||||||
|   "main": "keyfetch.js", |         "auth0", | ||||||
|   "name": "keyfetch", |         "pem", | ||||||
|   "scripts": { |         "RSA", | ||||||
|     "test": "echo \"Error: no test specified\" && exit 1" |         "EC", | ||||||
|   }, |         "ECDSA", | ||||||
|   "version": "1.1.2" |         "OIDC", | ||||||
|  |         "well-known" | ||||||
|  |     ], | ||||||
|  |     "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||||
|  |     "license": "MPL-2.0" | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user