Compare commits
	
		
			4 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| aba70bf0ff | |||
| a32b942861 | |||
| e274e5368a | |||
| e6de23532b | 
							
								
								
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | { | ||||||
|  |   "bracketSpacing": true, | ||||||
|  |   "printWidth": 120, | ||||||
|  |   "tabWidth": 4, | ||||||
|  |   "trailingComma": "none", | ||||||
|  |   "useTabs": false | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										90
									
								
								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) { | ||||||
| @ -46,8 +46,8 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) { | |||||||
| Quick JWT verification (for authentication): | Quick JWT verification (for authentication): | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var keyfetch = require('keyfetch'); | var keyfetch = require("keyfetch"); | ||||||
| var jwt = '...'; | var jwt = "..."; | ||||||
| 
 | 
 | ||||||
| keyfetch.jwt.verify(jwt).then(function (decoded) { | keyfetch.jwt.verify(jwt).then(function (decoded) { | ||||||
|     console.log(decoded); |     console.log(decoded); | ||||||
| @ -57,7 +57,7 @@ keyfetch.jwt.verify(jwt).then(function (decoded) { | |||||||
| JWT verification (for authorization): | JWT verification (for authorization): | ||||||
| 
 | 
 | ||||||
| ```js | ```js | ||||||
| var options = { issuers: ['https://example.com/'], claims: { role: 'admin' } }; | var options = { issuers: ["https://example.com/"], claims: { role: "admin" } }; | ||||||
| keyfetch.jwt.verify(jwt, options).then(function (decoded) { | keyfetch.jwt.verify(jwt, options).then(function (decoded) { | ||||||
|     console.log(decoded); |     console.log(decoded); | ||||||
| }); | }); | ||||||
| @ -66,19 +66,16 @@ 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( | keyfetch.oidcJwk(token.header.kid, token.payload.iss).then(function (result) { | ||||||
|   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); | ||||||
| @ -87,18 +84,19 @@ keyfetch.oidcJwk( | |||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| *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 | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -115,12 +113,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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -130,12 +128,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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -145,12 +143,12 @@ 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 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| @ -161,7 +159,7 @@ 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. | 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.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) { | ||||||
|     /* |     /* | ||||||
|     { protected: '...'  // base64 header |     { protected: '...'  // base64 header | ||||||
|     , payload: '...'    // base64 payload |     , payload: '...'    // base64 payload | ||||||
| @ -187,12 +185,12 @@ keyfetch.jwt.verify(jwt, { | |||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| * `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url. | -   `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) | -   `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) | -   `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) | -   `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) | -   `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 | -   `jwk` same as above, but a single key rather than a list | ||||||
| 
 | 
 | ||||||
| ### Decode JWT | ### Decode JWT | ||||||
| 
 | 
 | ||||||
| @ -217,16 +215,16 @@ It's easier just to show the code than to explain the example. | |||||||
| ```js | ```js | ||||||
| keyfetch.jwt.decode = function (jwt) { | keyfetch.jwt.decode = function (jwt) { | ||||||
|     // Unpack JWS from "compact" form |     // Unpack JWS from "compact" form | ||||||
|   var parts = jwt.split('.'); |     var parts = jwt.split("."); | ||||||
|     var obj = { |     var obj = { | ||||||
|     protected: parts[0] |         protected: parts[0], | ||||||
|   , payload: parts[1] |         payload: parts[1], | ||||||
|   , signature: parts[2] |         signature: parts[2] | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Decode JWT properties from JWS as unordered objects |     // Decode JWT properties from JWS as unordered objects | ||||||
|   obj.header = JSON.parse(Buffer.from(obj.protected, 'base64')); |     obj.header = JSON.parse(Buffer.from(obj.protected, "base64")); | ||||||
|   obj.claims = JSON.parse(Buffer.from(obj.payload, 'base64')); |     obj.claims = JSON.parse(Buffer.from(obj.payload, "base64")); | ||||||
| 
 | 
 | ||||||
|     return obj; |     return obj; | ||||||
| }; | }; | ||||||
| @ -237,14 +235,14 @@ keyfetch.jwt.decode = function (jwt) { | |||||||
| ```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. | ||||||
|  | |||||||
							
								
								
									
										142
									
								
								keyfetch-test.js
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								keyfetch-test.js
									
									
									
									
									
								
							| @ -1,10 +1,12 @@ | |||||||
| 'use strict'; | "use strict"; | ||||||
| 
 | 
 | ||||||
| var keyfetch = require('./keyfetch.js'); | var keyfetch = require("./keyfetch.js"); | ||||||
| var testIss = "https://example.auth0.com"; | var testIss = "https://example.auth0.com"; | ||||||
| 
 | 
 | ||||||
| keyfetch.init({}); | keyfetch.init({}); | ||||||
| keyfetch.oidcJwks(testIss).then(function (hits) { | keyfetch | ||||||
|  |     .oidcJwks(testIss) | ||||||
|  |     .then(function (hits) { | ||||||
|         keyfetch._clear(); |         keyfetch._clear(); | ||||||
|         //console.log(hits);
 |         //console.log(hits);
 | ||||||
|         return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () { |         return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () { | ||||||
| @ -12,54 +14,130 @@ keyfetch.oidcJwks(testIss).then(function (hits) { | |||||||
|                 //console.log(jwk);
 |                 //console.log(jwk);
 | ||||||
|             }); |             }); | ||||||
|         }); |         }); | ||||||
| }).then(function () { |     }) | ||||||
|  |     .then(function () { | ||||||
|         console.log("Fetching PASSES"); |         console.log("Fetching PASSES"); | ||||||
| }).catch(function (err) { |     }) | ||||||
|  |     .catch(function (err) { | ||||||
|         console.error("NONE SHALL PASS!"); |         console.error("NONE SHALL PASS!"); | ||||||
|         console.error(err); |         console.error(err); | ||||||
|         process.exit(1); |         process.exit(1); | ||||||
| }); |     }); | ||||||
| 
 | 
 | ||||||
| /*global Promise*/ | /*global Promise*/ | ||||||
| var keypairs = require('keypairs.js'); | var keypairs = require("keypairs.js"); | ||||||
| keypairs.generate().then(function (pair) { | keypairs.generate().then(function (pair) { | ||||||
|   return keypairs.signJwt({ |     return Promise.all([ | ||||||
|     jwk: pair.private, iss: 'https://example.com/', sub: 'mikey', exp: '1h' |         keypairs | ||||||
|   }).then(function (jwt) { |             .signJwt({ | ||||||
|  |                 jwk: pair.private, | ||||||
|  |                 iss: "https://example.com/", | ||||||
|  |                 sub: "mikey", | ||||||
|  |                 exp: "1h" | ||||||
|  |             }) | ||||||
|  |             .then(function (jwt) { | ||||||
|                 return Promise.all([ |                 return Promise.all([ | ||||||
|                     keyfetch.jwt.verify(jwt, { jwk: pair.public }).then(function (verified) { |                     keyfetch.jwt.verify(jwt, { jwk: pair.public }).then(function (verified) { | ||||||
|                         if (!(verified.claims && verified.claims.exp)) { |                         if (!(verified.claims && verified.claims.exp)) { | ||||||
|                             throw new Error("malformed decoded token"); |                             throw new Error("malformed decoded token"); | ||||||
|                         } |                         } | ||||||
|       }) |                     }), | ||||||
|     , keyfetch.jwt.verify(keyfetch.jwt.decode(jwt), { jwk: pair.public }).then(function (verified) { |                     keyfetch.jwt.verify(keyfetch.jwt.decode(jwt), { jwk: pair.public }).then(function (verified) { | ||||||
|                         if (!(verified.claims && verified.claims.exp)) { |                         if (!(verified.claims && verified.claims.exp)) { | ||||||
|                             throw new Error("malformed decoded token"); |                             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"] | ||||||
|                         }) |                         }) | ||||||
|     , keyfetch.jwt.verify(jwt, { jwks: [pair.public] }) |                         .then(e("bad scheme")) | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com/'] }) |                         .catch(throwIfNotExpected), | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com'] }) |                     keyfetch.jwt | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['*'] }) |                         .verify(jwt, { | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['http://example.com'] }) |                             jwk: pair.public, | ||||||
|         .then(e("bad scheme")).catch(throwIfNotExpected) |                             issuers: ["https://www.example.com"] | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://www.example.com'] }) |                         }) | ||||||
|         .then(e("bad prefix")).catch(throwIfNotExpected) |                         .then(e("bad prefix")) | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://wexample.com'] }) |                         .catch(throwIfNotExpected), | ||||||
|         .then(e("bad sld")).catch(throwIfNotExpected) |                     keyfetch.jwt | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.comm'] }) |                         .verify(jwt, { | ||||||
|         .then(e("bad tld")).catch(throwIfNotExpected) |                             jwk: pair.public, | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com/' } }) |                             issuers: ["https://wexample.com"] | ||||||
|     , keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com' } }) |                         }) | ||||||
|         .then(e("inexact claim")).catch(throwIfNotExpected) |                         .then(e("bad sld")) | ||||||
|     ]).then(function () { |                         .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"); |             console.log("JWT PASSES"); | ||||||
|     }).catch(function (err) { |         }) | ||||||
|  |         .catch(function (err) { | ||||||
|             console.error("NONE SHALL PASS!"); |             console.error("NONE SHALL PASS!"); | ||||||
|             console.error(err); |             console.error(err); | ||||||
|             process.exit(1); |             process.exit(1); | ||||||
|         }); |         }); | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
| /* | /* | ||||||
| var jwt = '...'; | var jwt = '...'; | ||||||
| @ -72,5 +150,7 @@ function e(msg) { | |||||||
|     return new Error("ETEST: " + msg); |     return new Error("ETEST: " + msg); | ||||||
| } | } | ||||||
| function throwIfNotExpected(err) { | function throwIfNotExpected(err) { | ||||||
|   if ("ETEST" === err.message.slice(0, 5)) { throw err; } |     if ("ETEST" === err.message.slice(0, 5)) { | ||||||
|  |         throw err; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										223
									
								
								keyfetch.js
									
									
									
									
									
								
							
							
						
						
									
										223
									
								
								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,7 +14,9 @@ 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) { return d; } |     if (!i && 0 !== i) { | ||||||
|  |         return d; | ||||||
|  |     } | ||||||
|     if (i >= n && i >= x) { |     if (i >= n && i >= x) { | ||||||
|         return parseInt(i, 10); |         return parseInt(i, 10); | ||||||
|     } else { |     } else { | ||||||
| @ -26,27 +28,15 @@ keyfetch._clear = function () { | |||||||
|     keyCache = {}; |     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 = 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) { | ||||||
| @ -58,33 +48,37 @@ 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(resp.body.keys.map(function (jwk) { |         return Promise.all( | ||||||
|  |             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(results.map(function (result) { |         return Promise.all( | ||||||
|  |             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)); | ||||||
|         }); |         }); | ||||||
| @ -93,9 +87,11 @@ 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(results.map(function (result) { |         return Promise.all( | ||||||
|  |             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)); | ||||||
|         }); |         }); | ||||||
| @ -105,9 +101,11 @@ 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(results.map(function (result) { |             return Promise.all( | ||||||
|  |                 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)); | ||||||
|             }); |             }); | ||||||
| @ -129,19 +127,25 @@ 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) { 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 = function (id, iss) { | ||||||
|     return keyfetch._checkCache(id, iss).then(function (hit) { |     return keyfetch._checkCache(id, iss).then(function (hit) { | ||||||
|     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 = function (id, jwksUrl) { | ||||||
|     return keyfetch._checkCache(id, jwksUrl).then(function (hit) { |     return keyfetch._checkCache(id, jwksUrl).then(function (hit) { | ||||||
|     if (hit) { return hit; } |         if (hit) { | ||||||
|  |             return hit; | ||||||
|  |         } | ||||||
|         return keyfetch.jwks(jwksUrl).then(checkId(id)); |         return keyfetch.jwks(jwksUrl).then(checkId(id)); | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| @ -151,7 +155,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; | ||||||
| @ -189,7 +193,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) { | ||||||
| @ -198,37 +202,44 @@ 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("'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing."); |         throw new Error( | ||||||
|  |             "'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing." | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
|   return iss.replace(/\/$/, ''); |     return iss.replace(/\/$/, ""); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| keyfetch.jwt = {}; | keyfetch.jwt = {}; | ||||||
| keyfetch.jwt.decode = function (jwt) { | keyfetch.jwt.decode = function (jwt) { | ||||||
|   var parts = jwt.split('.'); |     var parts = jwt.split("."); | ||||||
|     // JWS
 |     // JWS
 | ||||||
|     var obj = { |     var obj = { | ||||||
|     protected: parts[0] |         protected: parts[0], | ||||||
|   , payload: parts[1] |         payload: parts[1], | ||||||
|   , signature: parts[2] |         signature: parts[2] | ||||||
|     }; |     }; | ||||||
|     // JWT
 |     // JWT
 | ||||||
|   obj.header = JSON.parse(Buffer.from(obj.protected, 'base64')); |     obj.header = JSON.parse(Buffer.from(obj.protected, "base64")); | ||||||
|   obj.claims = JSON.parse(Buffer.from(obj.payload, 'base64')); |     obj.claims = JSON.parse(Buffer.from(obj.payload, "base64")); | ||||||
|     return obj; |     return obj; | ||||||
| }; | }; | ||||||
| keyfetch.jwt.verify = function (jwt, opts) { | keyfetch.jwt.verify = function (jwt, opts) { | ||||||
|   if (!opts) { opts = {}; } |     if (!opts) { | ||||||
|  |         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 active; | ||||||
|     var issuers = opts.issuers || ['*']; |         var issuers = opts.issuers || ["*"]; | ||||||
|         var claims = opts.claims || {}; |         var claims = opts.claims || {}; | ||||||
|     if (!jwt || 'string' === typeof jwt) { |         if (!jwt || "string" === typeof jwt) { | ||||||
|       try { decoded = keyfetch.jwt.decode(jwt); } |             try { | ||||||
|       catch (e) { throw new Error("could not parse jwt: '" + jwt + "'"); } |                 decoded = keyfetch.jwt.decode(jwt); | ||||||
|  |             } catch (e) { | ||||||
|  |                 throw new Error("could not parse jwt: '" + jwt + "'"); | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             decoded = jwt; |             decoded = jwt; | ||||||
|         } |         } | ||||||
| @ -239,15 +250,17 @@ keyfetch.jwt.verify = function (jwt, opts) { | |||||||
|             throw new Error("token was issued by an untrusted issuer: '" + decoded.claims.iss + "'"); |             throw new Error("token was issued by an untrusted issuer: '" + decoded.claims.iss + "'"); | ||||||
|         } |         } | ||||||
|         // TODO verify claims also?
 |         // TODO verify claims also?
 | ||||||
|     if (!Object.keys(claims).every(function (key) { |         if ( | ||||||
|  |             !Object.keys(claims).every(function (key) { | ||||||
|                 if (claims[key] === decoded.claims[key]) { |                 if (claims[key] === decoded.claims[key]) { | ||||||
|                     return true; |                     return true; | ||||||
|                 } |                 } | ||||||
|     })) { |             }) | ||||||
|  |         ) { | ||||||
|             throw new Error("token did not match on one or more authorization claims: '" + Object.keys(claims) + "'"); |             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); |         active = (opts.exp || 0) + parseInt(exp, 10) - Date.now() / 1000 > 0; | ||||||
|         if (!active) { |         if (!active) { | ||||||
|             // expiration was on the token or, if not, such a token is not allowed
 |             // expiration was on the token or, if not, such a token is not allowed
 | ||||||
|             if (exp || false !== opts.exp) { |             if (exp || false !== opts.exp) { | ||||||
| @ -255,7 +268,7 @@ keyfetch.jwt.verify = function (jwt, opts) { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (nbf) { |         if (nbf) { | ||||||
|       active = (parseInt(nbf, 10) - (Date.now()/1000) <= 0); |             active = parseInt(nbf, 10) - Date.now() / 1000 <= 0; | ||||||
|             if (!active) { |             if (!active) { | ||||||
|                 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 + "'"); | ||||||
|             } |             } | ||||||
| @ -268,11 +281,11 @@ keyfetch.jwt.verify = function (jwt, opts) { | |||||||
|         var iss; |         var iss; | ||||||
|         var fetcher; |         var fetcher; | ||||||
|         var fetchOne; |         var fetchOne; | ||||||
|     if (!opts.strategy || 'oidc' === opts.strategy) { |         if (!opts.strategy || "oidc" === opts.strategy) { | ||||||
|             iss = decoded.claims.iss; |             iss = decoded.claims.iss; | ||||||
|             fetcher = keyfetch.oidcJwks; |             fetcher = keyfetch.oidcJwks; | ||||||
|             fetchOne = keyfetch.oidcJwk; |             fetchOne = keyfetch.oidcJwk; | ||||||
|     } else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) { |         } else if ("auth0" === opts.strategy || "well-known" === opts.strategy) { | ||||||
|             iss = decoded.claims.iss; |             iss = decoded.claims.iss; | ||||||
|             fetcher = keyfetch.wellKnownJwks; |             fetcher = keyfetch.wellKnownJwks; | ||||||
|             fetchOne = keyfetch.wellKnownJwk; |             fetchOne = keyfetch.wellKnownJwk; | ||||||
| @ -294,45 +307,54 @@ keyfetch.jwt.verify = function (jwt, opts) { | |||||||
|             if (true === keyfetch.jws.verify(decoded, hit)) { |             if (true === keyfetch.jws.verify(decoded, hit)) { | ||||||
|                 return decoded; |                 return decoded; | ||||||
|             } |             } | ||||||
|       throw new Error('token signature verification was unsuccessful'); |             throw new Error("token signature verification was unsuccessful"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         function verifyAny(hits) { |         function verifyAny(hits) { | ||||||
|       if (hits.some(function (hit) { |             if ( | ||||||
|  |                 hits.some(function (hit) { | ||||||
|                     if (kid) { |                     if (kid) { | ||||||
|           if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { return; } |                         if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { | ||||||
|           if (true === keyfetch.jws.verify(decoded, hit)) { return true; } |                             return; | ||||||
|           throw new Error('token signature verification was unsuccessful'); |  | ||||||
|         } else { |  | ||||||
|           if (true === keyfetch.jws.verify(decoded, hit)) { return true; } |  | ||||||
|                         } |                         } | ||||||
|       })) { |                         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) { |         function overrideLookup(jwks) { | ||||||
|       return Promise.all(jwks.map(function (jwk) { |             return Promise.all( | ||||||
|  |                 jwks.map(function (jwk) { | ||||||
|                     var Keypairs = jwk.x ? Eckles : Rasha; |                     var Keypairs = jwk.x ? Eckles : Rasha; | ||||||
|                     return Keypairs.export({ jwk: jwk }).then(function (pem) { |                     return Keypairs.export({ jwk: jwk }).then(function (pem) { | ||||||
|                         return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { |                         return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { | ||||||
|                             return { jwk: jwk, pem: pem, thumbprint: thumb }; |                             return { jwk: jwk, pem: pem, thumbprint: thumb }; | ||||||
|                         }); |                         }); | ||||||
|                     }); |                     }); | ||||||
|       })).then(verifyAny); |                 }) | ||||||
|  |             ).then(verifyAny); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| keyfetch.jws = {}; | keyfetch.jws = {}; | ||||||
| keyfetch.jws.verify = function (jws, pub) { | keyfetch.jws.verify = function (jws, pub) { | ||||||
|   var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); |     var alg = "SHA" + jws.header.alg.replace(/[^\d]+/i, ""); | ||||||
|   var sig = ecdsaAsn1SigToJwtSig(jws.header, jws.signature); |     var sig = ecdsaJoseSigToAsn1Sig(jws.header, jws.signature); | ||||||
|   return require('crypto') |     return require("crypto") | ||||||
|         .createVerify(alg) |         .createVerify(alg) | ||||||
|     .update(jws.protected + '.' + jws.payload) |         .update(jws.protected + "." + jws.payload) | ||||||
|     .verify(pub.pem, sig, 'base64') |         .verify(pub.pem, sig, "base64"); | ||||||
|   ; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| // old, gotta make sure nothing else uses this
 | // old, gotta make sure nothing else uses this
 | ||||||
| @ -347,46 +369,59 @@ keyfetch.verify = function (opts) { | |||||||
|     }); |     }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| function ecdsaAsn1SigToJwtSig(header, b64sig) { | function ecdsaJoseSigToAsn1Sig(header, b64sig) { | ||||||
|     // ECDSA JWT signatures differ from "normal" ECDSA signatures
 |     // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||||
|     // https://tools.ietf.org/html/rfc7518#section-3.4
 |     // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||||
|   if (!/^ES/i.test(header.alg)) { return b64sig; } |     if (!/^ES/i.test(header.alg)) { | ||||||
|  |         return b64sig; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|   var bufsig = Buffer.from(b64sig, 'base64'); |     var bufsig = Buffer.from(b64sig, "base64"); | ||||||
|     var hlen = bufsig.byteLength / 2; // should be even
 |     var hlen = bufsig.byteLength / 2; // should be even
 | ||||||
|     var r = bufsig.slice(0, hlen); |     var r = bufsig.slice(0, hlen); | ||||||
|     var s = bufsig.slice(hlen); |     var s = bufsig.slice(hlen); | ||||||
|     // unpad positive ints less than 32 bytes wide
 |     // unpad positive ints less than 32 bytes wide
 | ||||||
|   while (!r[0]) { r = r.slice(1); } |     while (!r[0]) { | ||||||
|   while (!s[0]) { s = s.slice(1); } |         r = r.slice(1); | ||||||
|  |     } | ||||||
|  |     while (!s[0]) { | ||||||
|  |         s = s.slice(1); | ||||||
|  |     } | ||||||
|     // pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide
 |     // 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 & r[0]) { | ||||||
|   if (0x80 & s[0]) { s = Buffer.concat([Buffer.from([0]), s]); } |         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 len = 2 + r.byteLength + 2 + s.byteLength; | ||||||
|     var head = [0x30]; |     var head = [0x30]; | ||||||
|     // hard code 0x80 + 1 because it won't be longer than
 |     // hard code 0x80 + 1 because it won't be longer than
 | ||||||
|     // two SHA512 plus two pad bytes (130 bytes <= 256)
 |     // two SHA512 plus two pad bytes (130 bytes <= 256)
 | ||||||
|   if (len >= 0x80) { head.push(0x81); } |     if (len >= 0x80) { | ||||||
|  |         head.push(0x81); | ||||||
|  |     } | ||||||
|     head.push(len); |     head.push(len); | ||||||
| 
 | 
 | ||||||
|     var buf = Buffer.concat([ |     var buf = Buffer.concat([ | ||||||
|     Buffer.from(head) |         Buffer.from(head), | ||||||
|   , Buffer.from([0x02, r.byteLength]), r |         Buffer.from([0x02, r.byteLength]), | ||||||
|   , Buffer.from([0x02, s.byteLength]), s |         r, | ||||||
|  |         Buffer.from([0x02, s.byteLength]), | ||||||
|  |         s | ||||||
|     ]); |     ]); | ||||||
| 
 | 
 | ||||||
|   return buf.toString('base64') |     return buf.toString("base64").replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, ""); | ||||||
|     .replace(/-/g, '+') |  | ||||||
|     .replace(/_/g, '/') |  | ||||||
|     .replace(/=/g, '') |  | ||||||
|   ; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function isTrustedIssuer(issuer) { | function isTrustedIssuer(issuer) { | ||||||
|     return function (trusted) { |     return function (trusted) { | ||||||
|     if ('*' === trusted) { return true; } |         if ("*" === trusted) { | ||||||
|     // TODO normalize and account for '*'
 |             return true; | ||||||
|     return issuer.replace(/\/$/, '') === trusted.replace(/\/$/, '') && trusted; |         } | ||||||
|  |         // TODO account for '*.example.com'
 | ||||||
|  |         trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted; | ||||||
|  |         return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted; | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "keyfetch", |   "name": "keyfetch", | ||||||
|   "version": "1.2.0", |   "version": "1.2.1", | ||||||
|   "description": "Lightweight support for fetching JWKs.", |   "description": "Lightweight support for fetching JWKs.", | ||||||
|   "homepage": "https://git.coolaj86.com/coolaj86/keyfetch.js", |   "homepage": "https://git.coolaj86.com/coolaj86/keyfetch.js", | ||||||
|   "main": "keyfetch.js", |   "main": "keyfetch.js", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user