make Prettier

This commit is contained in:
AJ ONeal 2020-04-08 16:01:06 -04:00
parent a32b942861
commit aba70bf0ff
4 changed files with 554 additions and 450 deletions

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"bracketSpacing": true,
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

View File

@ -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.

View File

@ -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,65 +14,126 @@ 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 Promise.all([ return Promise.all([
keypairs.signJwt({ keypairs
jwk: pair.private, iss: 'https://example.com/', sub: 'mikey', exp: '1h' .signJwt({
}).then(function (jwt) { 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: ['example.com'] }) .verify(jwt, {
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['example.com/'] }) jwk: pair.public,
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['*'] }) issuers: ["https://www.example.com"]
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['http://example.com'] }) })
.then(e("bad scheme")).catch(throwIfNotExpected) .then(e("bad prefix"))
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://www.example.com'] }) .catch(throwIfNotExpected),
.then(e("bad prefix")).catch(throwIfNotExpected) keyfetch.jwt
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://wexample.com'] }) .verify(jwt, {
.then(e("bad sld")).catch(throwIfNotExpected) jwk: pair.public,
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.comm'] }) issuers: ["https://wexample.com"]
.then(e("bad tld")).catch(throwIfNotExpected) })
, keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com/' } }) .then(e("bad sld"))
, keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com' } }) .catch(throwIfNotExpected),
.then(e("inexact claim")).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"
}) })
, keypairs.signJwt({ .then(function (jwt) {
jwk: pair.private, iss: false, sub: 'mikey', exp: '1h'
}).then(function (jwt) {
return Promise.all([ return Promise.all([
keyfetch.jwt.verify(jwt, { jwk: pair.public }) keyfetch.jwt.verify(jwt, { jwk: pair.public }),
, keyfetch.jwt.verify(jwt) keyfetch.jwt.verify(jwt).then(e("should have an issuer")).catch(throwIfNotExpected),
.then(e("should have an issuer")).catch(throwIfNotExpected) keyfetch.jwt
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com/'] }) .verify(jwt, {
.then(e("fail when issuer specified and doesn't exist")).catch(throwIfNotExpected) jwk: pair.public,
issuers: ["https://example.com/"]
})
.then(e("fail when issuer specified and doesn't exist"))
.catch(throwIfNotExpected)
]); ]);
}) })
]).then(function () { ])
.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);
@ -87,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;
}
} }

View File

@ -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 = ecdsaJoseSigToAsn1Sig(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
@ -350,44 +372,56 @@ keyfetch.verify = function (opts) {
function ecdsaJoseSigToAsn1Sig(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) {
return true;
}
// TODO account for '*.example.com' // TODO account for '*.example.com'
trusted = (/^http(s?):\/\//.test(trusted) ? trusted : ('https://' + trusted)); trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted;
return issuer.replace(/\/$/, '') === trusted.replace(/\/$/, '') && trusted; return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted;
}; };
} }