Compare commits
	
		
			No commits in common. "master" and "v1.1.2" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1 +0,0 @@ | |||||||
| node_modules |  | ||||||
| @ -1,7 +0,0 @@ | |||||||
| { |  | ||||||
|   "bracketSpacing": true, |  | ||||||
|   "printWidth": 120, |  | ||||||
|   "tabWidth": 4, |  | ||||||
|   "trailingComma": "none", |  | ||||||
|   "useTabs": false |  | ||||||
| } |  | ||||||
							
								
								
									
										41
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,41 +0,0 @@ | |||||||
| 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. |  | ||||||
							
								
								
									
										152
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								README.md
									
									
									
									
									
								
							| @ -9,17 +9,17 @@ 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) | ||||||
| -   [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) | * [ ] esoteric variants (excluded to keep the code featherweight and secure) | ||||||
| 
 | 
 | ||||||
| # Install | # Install | ||||||
| 
 | 
 | ||||||
| @ -32,7 +32,7 @@ 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) { | ||||||
| @ -43,22 +43,13 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) { | |||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Quick JWT verification (for authentication): | Quick JWT verification: | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var keyfetch = require("keyfetch"); | var keyfetch = require('keyfetch'); | ||||||
| var jwt = "..."; | var jwt = '...'; | ||||||
| 
 | 
 | ||||||
| keyfetch.jwt.verify(jwt).then(function (decoded) { | keyfetch.verify({ jwt: 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); |   console.log(decoded); | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| @ -66,37 +57,39 @@ keyfetch.jwt.verify(jwt, options).then(function (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(token.header.kid, token.payload.iss).then(function (result) { | keyfetch.oidcJwk( | ||||||
|  |   token.header.kid | ||||||
|  | , token.payload.iss | ||||||
|  | ).then(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); | ||||||
| 
 | 
 | ||||||
|     jwt.jwt.verify(jwt, { jwk: result.jwk }); |   jwt.verify(jwt, pem); | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| _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 ( |   return /^https:/.test(iss) &&         // must be a secure domain | ||||||
|         /^https:/.test(iss) && /(\.|^)example\.com$/.test(iss) // must be a secure domain |     /(\.|^)example\.com$/.test(iss);    // can be example.com or any subdomain | ||||||
|     ); // can be example.com or any subdomain |  | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -113,12 +106,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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -128,12 +121,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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -143,106 +136,37 @@ 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.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) { | keyfetch.verify({ jwt: jwk, strategy: 'oidc' }) | ||||||
|     /* | // Promises a decoded JWT { headers, payload, signature } or fails | ||||||
|     { protected: '...'  // base64 header |  | ||||||
|     , payload: '...'    // base64 payload |  | ||||||
|     , signature: '...'  // base64 signature |  | ||||||
|     , header: {...}     // decoded header |  | ||||||
|     , claims: {...}     // decoded payload |  | ||||||
|     } |  | ||||||
|   */ |  | ||||||
| }); |  | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| When used for authorization, it's important to specify which `issuers` are allowed | * `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url. | ||||||
| (otherwise anyone can create a valid token with whatever any claims they want). |  | ||||||
| 
 |  | ||||||
| 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/' ] |  | ||||||
| , 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) |  | ||||||
| -   `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. | ||||||
|  | |||||||
							
								
								
									
										152
									
								
								keyfetch-test.js
									
									
									
									
									
								
							
							
						
						
									
										152
									
								
								keyfetch-test.js
									
									
									
									
									
								
							| @ -1,156 +1,20 @@ | |||||||
| "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 | keyfetch.oidcJwks("https://bigsquid.auth0.com").then(function (jwks) { | ||||||
|     .oidcJwks(testIss) |   console.log(jwks); | ||||||
|     .then(function (hits) { |   return keyfetch.oidcJwk(jwks[0].thumbprint, "https://bigsquid.auth0.com").then(function (jwk) { | ||||||
|         keyfetch._clear(); |     console.log(jwk); | ||||||
|         //console.log(hits);
 |  | ||||||
|         return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () { |  | ||||||
|             return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function (/*jwk*/) { |  | ||||||
|                 //console.log(jwk);
 |  | ||||||
|   }); |   }); | ||||||
|         }); | }).catch(function (err) { | ||||||
|     }) |  | ||||||
|     .then(function () { |  | ||||||
|         console.log("Fetching PASSES"); |  | ||||||
|     }) |  | ||||||
|     .catch(function (err) { |  | ||||||
|         console.error("NONE SHALL PASS!"); |  | ||||||
|   console.error(err); |   console.error(err); | ||||||
|         process.exit(1); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
| /*global Promise*/ |  | ||||||
| var keypairs = require("keypairs.js"); |  | ||||||
| keypairs.generate().then(function (pair) { |  | ||||||
|     return Promise.all([ |  | ||||||
|         keypairs |  | ||||||
|             .signJwt({ |  | ||||||
|                 jwk: pair.private, |  | ||||||
|                 iss: "https://example.com/", |  | ||||||
|                 sub: "mikey", |  | ||||||
|                 exp: "1h" |  | ||||||
|             }) |  | ||||||
|             .then(function (jwt) { |  | ||||||
|                 return Promise.all([ |  | ||||||
|                     keyfetch.jwt.verify(jwt, { jwk: pair.public }).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 }).then(function (verified) { |  | ||||||
|                         if (!(verified.claims && verified.claims.exp)) { |  | ||||||
|                             throw new Error("malformed decoded token"); |  | ||||||
|                         } |  | ||||||
|                     }), |  | ||||||
|                     keyfetch.jwt.verify(jwt, { jwks: [pair.public] }), |  | ||||||
|                     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([ |  | ||||||
|                     keyfetch.jwt.verify(jwt, { jwk: pair.public }), |  | ||||||
|                     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; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										316
									
								
								keyfetch.js
									
									
									
									
									
								
							
							
						
						
									
										316
									
								
								keyfetch.js
									
									
									
									
									
								
							| @ -1,11 +1,11 @@ | |||||||
| "use strict"; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var keyfetch = module.exports; | var keyfetch = module.exports; | ||||||
| 
 | 
 | ||||||
| var promisify = require("util").promisify; | var promisify = require('util').promisify; | ||||||
| var requestAsync = promisify(require("@coolaj86/urequest")); | var requestAsync = promisify(require('@coolaj86/urequest')); | ||||||
| var Rasha = require("rasha"); | var Rasha = require('rasha'); | ||||||
| var Eckles = require("eckles"); | 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; | ||||||
| @ -14,9 +14,7 @@ var keyCache = {}; | |||||||
| /*global Promise*/ | /*global Promise*/ | ||||||
| function checkMinDefaultMax(opts, key, n, d, x) { | function checkMinDefaultMax(opts, key, n, d, x) { | ||||||
|   var i = opts[key]; |   var i = opts[key]; | ||||||
|     if (!i && 0 !== i) { |   if (!i && 0 !== i) { return d; } | ||||||
|         return d; |  | ||||||
|     } |  | ||||||
|   if (i >= n && i >= x) { |   if (i >= n && i >= x) { | ||||||
|     return parseInt(i, 10); |     return parseInt(i, 10); | ||||||
|   } else { |   } else { | ||||||
| @ -24,19 +22,28 @@ function checkMinDefaultMax(opts, key, n, d, x) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| keyfetch._clear = function () { |  | ||||||
|     keyCache = {}; |  | ||||||
| }; |  | ||||||
| keyfetch.init = function (opts) { | keyfetch.init = function (opts) { | ||||||
|     mincache = checkMinDefaultMax(opts, "mincache", 1 * 60, mincache, 31 * 24 * 60 * 60); |   mincache = checkMinDefaultMax(opts, 'mincache', | ||||||
|     maxcache = checkMinDefaultMax(opts, "maxcache", 1 * 60 * 60, maxcache, 31 * 24 * 60 * 60); |     1 * 60, | ||||||
|     staletime = checkMinDefaultMax(opts, "staletime", 1 * 60, staletime, 31 * 24 * 60 * 60); |     mincache, | ||||||
|  |     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 = function (iss) { | ||||||
|   return Promise.resolve().then(function () { |   return Promise.resolve().then(function () { | ||||||
|     return requestAsync({ |     return requestAsync({ | ||||||
|             url: normalizeIss(iss) + "/.well-known/openid-configuration", |       url: normalizeIss(iss) + '/.well-known/openid-configuration' | ||||||
|             json: true |     , json: true | ||||||
|     }).then(function (resp) { |     }).then(function (resp) { | ||||||
|       var oidcConf = resp.body; |       var oidcConf = resp.body; | ||||||
|       if (!oidcConf.jwks_uri) { |       if (!oidcConf.jwks_uri) { | ||||||
| @ -48,37 +55,33 @@ keyfetch._oidc = function (iss) { | |||||||
| }; | }; | ||||||
| keyfetch._wellKnownJwks = function (iss) { | keyfetch._wellKnownJwks = function (iss) { | ||||||
|   return Promise.resolve().then(function () { |   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 = function (iss) { | ||||||
|   return requestAsync({ url: iss, json: true }).then(function (resp) { |   return requestAsync({ url: iss, json: true }).then(function (resp) { | ||||||
|         return Promise.all( |     return Promise.all(resp.body.keys.map(function (jwk) { | ||||||
|             resp.body.keys.map(function (jwk) { |  | ||||||
|       // EC keys have an x values, whereas RSA keys do not
 |       // EC keys have an x values, whereas RSA keys do not
 | ||||||
|       var Keypairs = jwk.x ? Eckles : Rasha; |       var Keypairs = jwk.x ? Eckles : Rasha; | ||||||
|       return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) { |       return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) { | ||||||
|         return Keypairs.export({ jwk: jwk }).then(function (pem) { |         return Keypairs.export({ jwk: jwk }).then(function (pem) { | ||||||
|           var cacheable = { |           var cacheable = { | ||||||
|                             jwk: jwk, |             jwk: jwk | ||||||
|                             thumbprint: thumbprint, |           , thumbprint: thumbprint | ||||||
|                             pem: pem |           , pem: pem | ||||||
|           }; |           }; | ||||||
|           return cacheable; |           return cacheable; | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|             }) |     })); | ||||||
|         ); |  | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| keyfetch.jwks = function (jwkUrl) { | keyfetch.jwks = function (jwkUrl) { | ||||||
|   // TODO DRY up a bit
 |   // TODO DRY up a bit
 | ||||||
|   return keyfetch._jwks(jwkUrl).then(function (results) { |   return keyfetch._jwks(jwkUrl).then(function (results) { | ||||||
|         return Promise.all( |     return Promise.all(results.map(function (result) { | ||||||
|             results.map(function (result) { |  | ||||||
|       return keyfetch._setCache(result.jwk.iss || jwkUrl, result); |       return keyfetch._setCache(result.jwk.iss || jwkUrl, result); | ||||||
|             }) |     })).then(function () { | ||||||
|         ).then(function () { |  | ||||||
|       // cacheable -> hit (keep original externally immutable)
 |       // cacheable -> hit (keep original externally immutable)
 | ||||||
|       return JSON.parse(JSON.stringify(results)); |       return JSON.parse(JSON.stringify(results)); | ||||||
|     }); |     }); | ||||||
| @ -87,11 +90,9 @@ keyfetch.jwks = function (jwkUrl) { | |||||||
| keyfetch.wellKnownJwks = function (iss) { | keyfetch.wellKnownJwks = function (iss) { | ||||||
|   // TODO DRY up a bit
 |   // TODO DRY up a bit
 | ||||||
|   return keyfetch._wellKnownJwks(iss).then(function (results) { |   return keyfetch._wellKnownJwks(iss).then(function (results) { | ||||||
|         return Promise.all( |     return Promise.all(results.map(function (result) { | ||||||
|             results.map(function (result) { |  | ||||||
|       return keyfetch._setCache(result.jwk.iss || iss, result); |       return keyfetch._setCache(result.jwk.iss || iss, result); | ||||||
|             }) |     })).then(function () { | ||||||
|         ).then(function () { |  | ||||||
|       // result -> hit (keep original externally immutable)
 |       // result -> hit (keep original externally immutable)
 | ||||||
|       return JSON.parse(JSON.stringify(results)); |       return JSON.parse(JSON.stringify(results)); | ||||||
|     }); |     }); | ||||||
| @ -101,11 +102,9 @@ keyfetch.oidcJwks = function (iss) { | |||||||
|   return keyfetch._oidc(iss).then(function (oidcConf) { |   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) { |     return keyfetch._jwks(oidcConf.jwks_uri).then(function (results) { | ||||||
|             return Promise.all( |       return Promise.all(results.map(function (result) { | ||||||
|                 results.map(function (result) { |  | ||||||
|         return keyfetch._setCache(result.jwk.iss || iss, result); |         return keyfetch._setCache(result.jwk.iss || iss, result); | ||||||
|                 }) |       })).then(function () { | ||||||
|             ).then(function () { |  | ||||||
|         // result -> hit (keep original externally immutable)
 |         // result -> hit (keep original externally immutable)
 | ||||||
|         return JSON.parse(JSON.stringify(results)); |         return JSON.parse(JSON.stringify(results)); | ||||||
|       }); |       }); | ||||||
| @ -114,8 +113,9 @@ keyfetch.oidcJwks = function (iss) { | |||||||
| }; | }; | ||||||
| function checkId(id) { | function checkId(id) { | ||||||
|   return function (results) { |   return function (results) { | ||||||
|         var result = results.filter(function (result) { |     var result = results.some(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]; | ||||||
| 
 | 
 | ||||||
| @ -127,25 +127,19 @@ function checkId(id) { | |||||||
| } | } | ||||||
| keyfetch.oidcJwk = function (id, iss) { | keyfetch.oidcJwk = function (id, iss) { | ||||||
|   return keyfetch._checkCache(id, iss).then(function (hit) { |   return keyfetch._checkCache(id, iss).then(function (hit) { | ||||||
|         if (hit) { |     if (hit) { return hit; } | ||||||
|             return hit; |  | ||||||
|         } |  | ||||||
|     return keyfetch.oidcJwks(iss).then(checkId(id)); |     return keyfetch.oidcJwks(iss).then(checkId(id)); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| keyfetch.wellKnownJwk = function (id, iss) { | keyfetch.wellKnownJwk = function (id, iss) { | ||||||
|   return keyfetch._checkCache(id, iss).then(function (hit) { |   return keyfetch._checkCache(id, iss).then(function (hit) { | ||||||
|         if (hit) { |     if (hit) { return hit; } | ||||||
|             return hit; |  | ||||||
|         } |  | ||||||
|     return keyfetch.wellKnownJwks(iss).then(checkId(id)); |     return keyfetch.wellKnownJwks(iss).then(checkId(id)); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| keyfetch.jwk = function (id, jwksUrl) { | keyfetch.jwk = function (id, jwksUrl) { | ||||||
|   return keyfetch._checkCache(id, jwksUrl).then(function (hit) { |   return keyfetch._checkCache(id, jwksUrl).then(function (hit) { | ||||||
|         if (hit) { |     if (hit) { return hit; } | ||||||
|             return hit; |  | ||||||
|         } |  | ||||||
|     return keyfetch.jwks(jwksUrl).then(checkId(id)); |     return keyfetch.jwks(jwksUrl).then(checkId(id)); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| @ -155,7 +149,7 @@ keyfetch._checkCache = function (id, 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; | ||||||
| @ -193,7 +187,7 @@ keyfetch._setCache = function (iss, cacheable) { | |||||||
|   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) { | ||||||
| @ -202,226 +196,78 @@ function normalizeIss(iss) { | |||||||
|   // We also don't want to allow insecure issuers
 |   // We also don't want to allow insecure issuers
 | ||||||
|   if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) { |   if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) { | ||||||
|     // note, we wrap some things in promises just so we can throw here
 |     // note, we wrap some things in promises just so we can throw here
 | ||||||
|         throw new Error( |     throw new Error("'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing."); | ||||||
|             "'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing." |  | ||||||
|         ); |  | ||||||
|   } |   } | ||||||
|     return iss.replace(/\/$/, ""); |   return iss.replace(/\/$/, ''); | ||||||
| } | } | ||||||
| 
 | keyfetch._decode = function (jwt) { | ||||||
| keyfetch.jwt = {}; |   var parts = jwt.split('.'); | ||||||
| keyfetch.jwt.decode = function (jwt) { |   return { | ||||||
|     var parts = jwt.split("."); |     header: JSON.parse(Buffer.from(parts[0], 'base64')) | ||||||
|     // JWS
 |   , payload: JSON.parse(Buffer.from(parts[1], 'base64')) | ||||||
|     var obj = { |   , signature: parts[2] //Buffer.from(parts[2], 'base64')
 | ||||||
|         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; |  | ||||||
| }; | }; | ||||||
| keyfetch.jwt.verify = function (jwt, opts) { | keyfetch.verify = function (opts) { | ||||||
|     if (!opts) { |   var jwt = opts.jwt; | ||||||
|         opts = {}; |  | ||||||
|     } |  | ||||||
|   return Promise.resolve().then(function () { |   return Promise.resolve().then(function () { | ||||||
|     var decoded; |     var decoded; | ||||||
|     var exp; |     var exp; | ||||||
|     var nbf; |     var nbf; | ||||||
|         var active; |     var valid; | ||||||
|         var issuers = opts.issuers || ["*"]; |  | ||||||
|         var claims = opts.claims || {}; |  | ||||||
|         if (!jwt || "string" === typeof jwt) { |  | ||||||
|     try { |     try { | ||||||
|                 decoded = keyfetch.jwt.decode(jwt); |       decoded = keyfetch._decode(jwt); | ||||||
|  |       exp = decoded.payload.exp; | ||||||
|  |       nbf = decoded.payload.nbf; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|                 throw new Error("could not parse jwt: '" + jwt + "'"); |       throw new Error("could not parse opts.jwt: '" + jwt + "'"); | ||||||
|     } |     } | ||||||
|         } else { |     if (exp) { | ||||||
|             decoded = jwt; |       valid = (parseInt(exp, 10) - (Date.now()/1000) > 0); | ||||||
|         } |       if (!valid) { | ||||||
|         exp = decoded.claims.exp; |  | ||||||
|         nbf = decoded.claims.nbf; |  | ||||||
| 
 |  | ||||||
|         if (!issuers.some(isTrustedIssuer(decoded.claims.iss))) { |  | ||||||
|             throw new Error("token was issued by an untrusted issuer: '" + decoded.claims.iss + "'"); |  | ||||||
|         } |  | ||||||
|         // TODO verify claims also?
 |  | ||||||
|         if ( |  | ||||||
|             !Object.keys(claims).every(function (key) { |  | ||||||
|                 if (claims[key] === decoded.claims[key]) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             }) |  | ||||||
|         ) { |  | ||||||
|             throw new Error("token did not match on one or more authorization claims: '" + Object.keys(claims) + "'"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         active = (opts.exp || 0) + parseInt(exp, 10) - Date.now() / 1000 > 0; |  | ||||||
|         if (!active) { |  | ||||||
|             // expiration was on the token or, if not, such a token is not allowed
 |  | ||||||
|             if (exp || false !== opts.exp) { |  | ||||||
|         throw new Error("token's 'exp' has passed or could not parsed: '" + exp + "'"); |         throw new Error("token's 'exp' has passed or could not parsed: '" + exp + "'"); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     if (nbf) { |     if (nbf) { | ||||||
|             active = parseInt(nbf, 10) - Date.now() / 1000 <= 0; |       valid = (parseInt(nbf, 10) - (Date.now()/1000) <= 0); | ||||||
|             if (!active) { |       if (!valid) { | ||||||
|         throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'"); |         throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'"); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|         if (opts.jwks || opts.jwk) { |  | ||||||
|             return overrideLookup(opts.jwks || [opts.jwk]); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     var kid = decoded.header.kid; |     var kid = decoded.header.kid; | ||||||
|     var iss; |     var iss; | ||||||
|     var fetcher; |     var fetcher; | ||||||
|         var fetchOne; |     if (!opts.strategy || 'oidc' === opts.strategy) { | ||||||
|         if (!opts.strategy || "oidc" === opts.strategy) { |       iss = decoded.payload.iss; | ||||||
|             iss = decoded.claims.iss; |  | ||||||
|       fetcher = keyfetch.oidcJwks; |       fetcher = keyfetch.oidcJwks; | ||||||
|             fetchOne = keyfetch.oidcJwk; |     } else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) { | ||||||
|         } else if ("auth0" === opts.strategy || "well-known" === opts.strategy) { |       iss = decoded.payload.iss; | ||||||
|             iss = decoded.claims.iss; |  | ||||||
|       fetcher = keyfetch.wellKnownJwks; |       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 p; |       var alg = 'RSA-SHA' + decoded.header.alg.replace(/[^\d]+/i, ''); | ||||||
|  |       return require('crypto') | ||||||
|  |         .createVerify(alg) | ||||||
|  |         .update(jwt.split('.')[0] + '.' + payload) | ||||||
|  |         .verify(jwk.pem, decoded.signature, 'base64'); | ||||||
|  |     } | ||||||
|  |     return fetcher(iss).then(function (jwks) { | ||||||
|  |       var payload = jwt.split('.')[1]; // as string, as it was signed
 | ||||||
|  |       if (jwks.some(function (jwk) { | ||||||
|         if (kid) { |         if (kid) { | ||||||
|             p = fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
 |           if (kid !== jwk.kid && kid !== jwk.thumbprint) { return; } | ||||||
|  |           if (verify(jwk, payload)) { return true; } | ||||||
|  |           throw new Error('token signature verification was unsuccessful'); | ||||||
|         } else { |         } else { | ||||||
|             p = fetcher(iss).then(verifyAny); |           if (verify(jwk, payload)) { return true; } | ||||||
|         } |         } | ||||||
|         return p; |       })) { | ||||||
| 
 |  | ||||||
|         function verifyOne(hit) { |  | ||||||
|             if (true === keyfetch.jws.verify(decoded, hit)) { |  | ||||||
|                 return decoded; |  | ||||||
|             } |  | ||||||
|             throw new Error("token signature verification was unsuccessful"); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function verifyAny(hits) { |  | ||||||
|             if ( |  | ||||||
|                 hits.some(function (hit) { |  | ||||||
|                     if (kid) { |  | ||||||
|                         if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { |  | ||||||
|                             return; |  | ||||||
|                         } |  | ||||||
|                         if (true === keyfetch.jws.verify(decoded, hit)) { |  | ||||||
|                             return true; |  | ||||||
|                         } |  | ||||||
|                         throw new Error("token signature verification was unsuccessful"); |  | ||||||
|                     } else { |  | ||||||
|                         if (true === keyfetch.jws.verify(decoded, hit)) { |  | ||||||
|                             return true; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }) |  | ||||||
|             ) { |  | ||||||
|         return decoded; |         return decoded; | ||||||
|       } |       } | ||||||
|       throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); |       throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function overrideLookup(jwks) { |  | ||||||
|             return Promise.all( |  | ||||||
|                 jwks.map(function (jwk) { |  | ||||||
|                     var Keypairs = jwk.x ? Eckles : Rasha; |  | ||||||
|                     return Keypairs.export({ jwk: jwk }).then(function (pem) { |  | ||||||
|                         return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { |  | ||||||
|                             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 = function (opts) { |  | ||||||
|     var jwt = opts.jwt; |  | ||||||
|     return keyfetch.jwt.verify(jwt, opts).then(function (obj) { |  | ||||||
|         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; |  | ||||||
|     }; |  | ||||||
| } |  | ||||||
|  | |||||||
							
								
								
									
										20
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,23 +1,23 @@ | |||||||
| { | { | ||||||
|   "name": "keyfetch", |   "name": "keyfetch", | ||||||
|   "version": "1.1.8", |   "version": "1.1.0", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@coolaj86/urequest": { |     "@coolaj86/urequest": { | ||||||
|       "version": "1.3.7", |       "version": "1.3.6", | ||||||
|       "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", |       "resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz", | ||||||
|       "integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" |       "integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA==" | ||||||
|     }, |     }, | ||||||
|     "eckles": { |     "eckles": { | ||||||
|       "version": "1.4.1", |       "version": "1.4.0", | ||||||
|       "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", |       "resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz", | ||||||
|       "integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" |       "integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA==" | ||||||
|     }, |     }, | ||||||
|     "rasha": { |     "rasha": { | ||||||
|       "version": "1.2.4", |       "version": "1.2.1", | ||||||
|       "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz", |       "resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz", | ||||||
|       "integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q==" |       "integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag==" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										41
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								package.json
									
									
									
									
									
								
							| @ -1,23 +1,18 @@ | |||||||
| { | { "author": { | ||||||
|   "name": "keyfetch", |     "name": "AJ ONeal", | ||||||
|   "version": "1.2.1", |     "email": "solderjs@gmail.com" | ||||||
|   "description": "Lightweight support for fetching JWKs.", |   }, | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/keyfetch.js", |   "bundleDependencies": false, | ||||||
|   "main": "keyfetch.js", |  | ||||||
|   "files": [], |  | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@coolaj86/urequest": "^1.3.7", |     "@coolaj86/urequest": "^1.3.6", | ||||||
|     "eckles": "^1.4.1", |     "eckles": "^1.4.0", | ||||||
|     "rasha": "^1.2.4" |     "rasha": "^1.2.1" | ||||||
|   }, |  | ||||||
|   "devDependencies": {}, |  | ||||||
|   "scripts": { |  | ||||||
|     "test": "node keyfetch-test.js" |  | ||||||
|   }, |  | ||||||
|   "repository": { |  | ||||||
|     "type": "git", |  | ||||||
|     "url": "https://git.coolaj86.com/coolaj86/keyfetch.js.git" |  | ||||||
|   }, |   }, | ||||||
|  |   "deprecated": false, | ||||||
|  |   "description": "Lightweight support for fetching JWKs.", | ||||||
|  |   "files": [ | ||||||
|  |     "keyfetch-test.js" | ||||||
|  |   ], | ||||||
|   "keywords": [ |   "keywords": [ | ||||||
|     "jwks", |     "jwks", | ||||||
|     "jwk", |     "jwk", | ||||||
| @ -30,7 +25,11 @@ | |||||||
|     "OIDC", |     "OIDC", | ||||||
|     "well-known" |     "well-known" | ||||||
|   ], |   ], | ||||||
|   "author": "AJ ONeal <solderjs@gmail.com> (https://coolaj86.com/)", |   "license": "MPL-2.0", | ||||||
|   "license": "MPL-2.0" |   "main": "keyfetch.js", | ||||||
|  |   "name": "keyfetch", | ||||||
|  |   "scripts": { | ||||||
|  |     "test": "echo \"Error: no test specified\" && exit 1" | ||||||
|  |   }, | ||||||
|  |   "version": "1.1.2" | ||||||
| } | } | ||||||
| 
 |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user