v1.2.0: add code to check issuers and claims
This commit is contained in:
		
							parent
							
								
									9b77939455
								
							
						
					
					
						commit
						5060c505b6
					
				
							
								
								
									
										88
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								README.md
									
									
									
									
									
								
							| @ -43,13 +43,22 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) { | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| Quick JWT verification: | ||||
| Quick JWT verification (for authentication): | ||||
| 
 | ||||
| ```js | ||||
| var keyfetch = require('keyfetch'); | ||||
| var jwt = '...'; | ||||
| 
 | ||||
| keyfetch.verify({ jwt: jwt }).then(function (decoded) { | ||||
| keyfetch.jwt.verify(jwt).then(function (decoded) { | ||||
|   console.log(decoded); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| JWT verification (for authorization): | ||||
| 
 | ||||
| ```js | ||||
| var options = { issuers: ['https://example.com/'], claims: { role: 'admin' } }; | ||||
| keyfetch.jwt.verify(jwt, options).then(function (decoded) { | ||||
|   console.log(decoded); | ||||
| }); | ||||
| ``` | ||||
| @ -74,7 +83,7 @@ keyfetch.oidcJwk( | ||||
|   console.log(result.thumprint); | ||||
|   console.log(result.pem); | ||||
| 
 | ||||
|   jwt.verify(jwt, pem); | ||||
|   jwt.jwt.verify(jwt, { jwk: result.jwk }); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| @ -147,12 +156,81 @@ keyfetch.oidcJwk(id, issuerUrl) | ||||
| 
 | ||||
| ### Verify JWT | ||||
| 
 | ||||
| This can accept a _JWT string_ (compact JWS) or a _decoded JWT object_ (JWS). | ||||
| 
 | ||||
| This can be used purely for verifying pure authentication tokens, as well as authorization tokens. | ||||
| 
 | ||||
| ```js | ||||
| keyfetch.verify({ jwt: jwk, strategy: 'oidc' }) | ||||
| // Promises a decoded JWT { headers, payload, signature } or fails | ||||
| keyfetch.jwt.verify(jwt, { strategy: 'oidc' }).then(function (verified) { | ||||
|   /* | ||||
|     { protected: '...'  // base64 header | ||||
|     , payload: '...'    // base64 payload | ||||
|     , signature: '...'  // base64 signature | ||||
|     , header: {...}     // decoded header | ||||
|     , claims: {...}     // decoded payload | ||||
|     } | ||||
|   */ | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| When used for authorization, it's important to specify which `issuers` are allowed | ||||
| (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 | ||||
| 
 | ||||
|  | ||||
| @ -6,19 +6,71 @@ var testIss = "https://example.auth0.com"; | ||||
| keyfetch.init({}); | ||||
| keyfetch.oidcJwks(testIss).then(function (hits) { | ||||
|   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 (jwk) { | ||||
|       console.log(jwk); | ||||
|     return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function (/*jwk*/) { | ||||
|       //console.log(jwk);
 | ||||
|     }); | ||||
|   }); | ||||
| }).then(function () { | ||||
|   console.log("Fetching PASSES"); | ||||
| }).catch(function (err) { | ||||
|   console.error("NONE SHALL PASS!"); | ||||
|   console.error(err); | ||||
|   process.exit(1); | ||||
| }); | ||||
| 
 | ||||
| /*global Promise*/ | ||||
| var keypairs = require('keypairs.js'); | ||||
| keypairs.generate().then(function (pair) { | ||||
|   return 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: ['*'] }) | ||||
|     , 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) | ||||
|     ]).then(function () { | ||||
|       console.log("JWT PASSES"); | ||||
|     }).catch(function (err) { | ||||
|       console.error("NONE SHALL PASS!"); | ||||
|       console.error(err); | ||||
|       process.exit(1); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| /* | ||||
| var jwt = '...'; | ||||
| keyfetch.verify({ jwt: jwt }).catch(function (err) { | ||||
|   console.log(err); | ||||
| }); | ||||
| */ | ||||
| 
 | ||||
| function e(msg) { | ||||
|   return new Error("ETEST: " + msg); | ||||
| } | ||||
| function throwIfNotExpected(err) { | ||||
|   if ("ETEST" === err.message.slice(0, 5)) { throw err; } | ||||
| } | ||||
|  | ||||
							
								
								
									
										196
									
								
								keyfetch.js
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								keyfetch.js
									
									
									
									
									
								
							| @ -202,45 +202,116 @@ function normalizeIss(iss) { | ||||
|   } | ||||
|   return iss.replace(/\/$/, ''); | ||||
| } | ||||
| keyfetch._decode = function (jwt) { | ||||
| 
 | ||||
| keyfetch.jwt = {}; | ||||
| keyfetch.jwt.decode = function (jwt) { | ||||
|   var parts = jwt.split('.'); | ||||
|   return { | ||||
|     header: JSON.parse(Buffer.from(parts[0], 'base64')) | ||||
|   , payload: JSON.parse(Buffer.from(parts[1], 'base64')) | ||||
|   , signature: parts[2] //Buffer.from(parts[2], 'base64')
 | ||||
|   // JWS
 | ||||
|   var obj = { | ||||
|     protected: parts[0] | ||||
|   , payload: parts[1] | ||||
|   , signature: parts[2] | ||||
|   }; | ||||
|   // JWT
 | ||||
|   obj.header = JSON.parse(Buffer.from(obj.protected, 'base64')); | ||||
|   obj.claims = JSON.parse(Buffer.from(obj.payload, 'base64')); | ||||
|   return obj; | ||||
| }; | ||||
| keyfetch.verify = function (opts) { | ||||
|   var jwt = opts.jwt; | ||||
| keyfetch.jwt.verify = function (jwt, opts) { | ||||
|   if (!opts) { opts = {}; } | ||||
|   return Promise.resolve().then(function () { | ||||
|     var decoded; | ||||
|     var exp; | ||||
|     var nbf; | ||||
|     var valid; | ||||
|     try { | ||||
|       decoded = keyfetch._decode(jwt); | ||||
|       exp = decoded.payload.exp; | ||||
|       nbf = decoded.payload.nbf; | ||||
|     } catch (e) { | ||||
|       throw new Error("could not parse opts.jwt: '" + jwt + "'"); | ||||
|     var active; | ||||
|     var issuers = opts.issuers || ['*']; | ||||
|     var claims = opts.claims || {}; | ||||
|     if (!jwt || 'string' === typeof jwt) { | ||||
|       try { decoded = keyfetch.jwt.decode(jwt); } | ||||
|       catch (e) { throw new Error("could not parse jwt: '" + jwt + "'"); } | ||||
|     } else { | ||||
|       decoded = jwt; | ||||
|     } | ||||
|     if (exp) { | ||||
|       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 + "'"); | ||||
|       } | ||||
|     } | ||||
|     if (nbf) { | ||||
|       valid = (parseInt(nbf, 10) - (Date.now()/1000) <= 0); | ||||
|       if (!valid) { | ||||
|       active = (parseInt(nbf, 10) - (Date.now()/1000) <= 0); | ||||
|       if (!active) { | ||||
|         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 iss; | ||||
|     var fetcher; | ||||
|     var fetchOne; | ||||
|     if (!opts.strategy || 'oidc' === opts.strategy) { | ||||
|       iss = decoded.claims.iss; | ||||
|       fetcher = keyfetch.oidcJwks; | ||||
|       fetchOne = keyfetch.oidcJwk; | ||||
|     } else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) { | ||||
|       iss = decoded.claims.iss; | ||||
|       fetcher = keyfetch.wellKnownJwks; | ||||
|       fetchOne = keyfetch.wellKnownJwk; | ||||
|     } else { | ||||
|       iss = opts.strategy; | ||||
|       fetcher = keyfetch.jwks; | ||||
|       fetchOne = keyfetch.jwk; | ||||
|     } | ||||
| 
 | ||||
|     var p; | ||||
|     if (kid) { | ||||
|       p = fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
 | ||||
|     } else { | ||||
|       p = fetcher(iss).then(verifyAny); | ||||
|     } | ||||
|     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; | ||||
|       } | ||||
|       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; | ||||
| @ -251,63 +322,28 @@ keyfetch.verify = function (opts) { | ||||
|         }); | ||||
|       })).then(verifyAny); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| keyfetch.jws = {}; | ||||
| keyfetch.jws.verify = function (jws, pub) { | ||||
|   var alg = 'SHA' + jws.header.alg.replace(/[^\d]+/i, ''); | ||||
|   var sig = ecdsaAsn1SigToJwtSig(jws.header, jws.signature); | ||||
|   return require('crypto') | ||||
|     .createVerify(alg) | ||||
|     .update(jws.protected + '.' + jws.payload) | ||||
|     .verify(pub.pem, sig, 'base64') | ||||
|   ; | ||||
| }; | ||||
| 
 | ||||
|     var kid = decoded.header.kid; | ||||
|     var iss; | ||||
|     var fetcher; | ||||
|     var fetchOne; | ||||
|     if (!opts.strategy || 'oidc' === opts.strategy) { | ||||
|       iss = decoded.payload.iss; | ||||
|       fetcher = keyfetch.oidcJwks; | ||||
|       fetchOne = keyfetch.oidcJwk; | ||||
|     } else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) { | ||||
|       iss = decoded.payload.iss; | ||||
|       fetcher = keyfetch.wellKnownJwks; | ||||
|       fetchOne = keyfetch.wellKnownJwk; | ||||
|     } else { | ||||
|       iss = opts.strategy; | ||||
|       fetcher = keyfetch.jwks; | ||||
|       fetchOne = keyfetch.jwk; | ||||
|     } | ||||
| 
 | ||||
|     var payload = jwt.split('.')[1]; // as string, as it was signed
 | ||||
|     if (kid) { | ||||
|       return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
 | ||||
|     } else { | ||||
|       return fetcher(iss).then(verifyAny); | ||||
|     } | ||||
| 
 | ||||
|     function verify(hit, payload) { | ||||
|       var alg = 'SHA' + decoded.header.alg.replace(/[^\d]+/i, ''); | ||||
|       var sig = ecdsaAsn1SigToJwtSig(decoded.header, decoded.signature); | ||||
|       return require('crypto') | ||||
|         .createVerify(alg) | ||||
|         .update(jwt.split('.')[0] + '.' + payload) | ||||
|         .verify(hit.pem, sig, 'base64') | ||||
|       ; | ||||
|     } | ||||
| 
 | ||||
|     function verifyOne(hit) { | ||||
|       if (true === verify(hit, payload)) { | ||||
|         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 === verify(hit, payload)) { return true; } | ||||
|           throw new Error('token signature verification was unsuccessful'); | ||||
|         } else { | ||||
|           if (true === verify(hit, payload)) { return true; } | ||||
|         } | ||||
|       })) { | ||||
|         return decoded; | ||||
|       } | ||||
|       throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token."); | ||||
|     } | ||||
| // 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 }; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| @ -346,3 +382,11 @@ function ecdsaAsn1SigToJwtSig(header, b64sig) { | ||||
|     .replace(/=/g, '') | ||||
|   ; | ||||
| } | ||||
| 
 | ||||
| function isTrustedIssuer(issuer) { | ||||
|   return function (trusted) { | ||||
|     if ('*' === trusted) { return true; } | ||||
|     // TODO normalize and account for '*'
 | ||||
|     return issuer.replace(/\/$/, '') === trusted.replace(/\/$/, '') && trusted; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "keyfetch", | ||||
|   "version": "1.1.10", | ||||
|   "version": "1.2.0", | ||||
|   "description": "Lightweight support for fetching JWKs.", | ||||
|   "homepage": "https://git.coolaj86.com/coolaj86/keyfetch.js", | ||||
|   "main": "keyfetch.js", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user