security warning -> issuer error, async/await

This commit is contained in:
AJ ONeal 2021-06-15 17:22:38 -06:00
parent a648de58d8
commit 842807f92b
3 changed files with 221 additions and 257 deletions

View File

@ -171,8 +171,8 @@ keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
}); });
``` ```
When used for authorization, it's important to specify which `issuers` are allowed When used for authorization, it's important to specify a limited set of trusted `issuers`. \
(otherwise anyone can create a valid token with whatever any claims they want). When using for federated authentication you may set `issuers = ["*"]` - but **DO NOT** trust claims such as `email` and `email_verified`.
If your authorization `claims` can be expressed as exact string matches, you can specify those too. If your authorization `claims` can be expressed as exact string matches, you can specify those too.

View File

@ -120,17 +120,12 @@ keypairs.generate().then(function (pair) {
exp: "1h" exp: "1h"
}) })
.then(function (jwt) { .then(function (jwt) {
var warned = false;
console.warn = function () {
warned = true;
};
return Promise.all([ return Promise.all([
// test that the old behavior of defaulting to '*' still works // test that the old behavior of defaulting to '*' still works
keyfetch.jwt.verify(jwt, { jwk: pair.public }).then(function () { keyfetch.jwt
if (!warned) { .verify(jwt, { jwk: pair.public })
throw e("should have issued security warning about allow all by default"); .then(e("should have issued security warning about allow all by default"))
} .catch(throwIfNotExpected),
}),
keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ["*"] }), keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ["*"] }),
keyfetch.jwt.verify(jwt).then(e("should have an issuer")).catch(throwIfNotExpected), keyfetch.jwt.verify(jwt).then(e("should have an issuer")).catch(throwIfNotExpected),
keyfetch.jwt keyfetch.jwt

View File

@ -11,7 +11,6 @@ var maxcache = 3 * 24 * 60 * 60;
var staletime = 15 * 60; var staletime = 15 * 60;
var keyCache = {}; var keyCache = {};
/*global Promise*/
function checkMinDefaultMax(opts, key, n, d, x) { function checkMinDefaultMax(opts, key, n, d, x) {
var i = opts[key]; var i = opts[key];
if (!i && 0 !== i) { if (!i && 0 !== i) {
@ -32,85 +31,70 @@ keyfetch.init = function (opts) {
maxcache = checkMinDefaultMax(opts, "maxcache", 1 * 60 * 60, maxcache, 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); staletime = checkMinDefaultMax(opts, "staletime", 1 * 60, staletime, 31 * 24 * 60 * 60);
}; };
keyfetch._oidc = function (iss) { keyfetch._oidc = async function (iss) {
return Promise.resolve().then(function () { var resp = await requestAsync({
return requestAsync({
url: normalizeIss(iss) + "/.well-known/openid-configuration", url: normalizeIss(iss) + "/.well-known/openid-configuration",
json: true json: true
}).then(function (resp) { });
var oidcConf = resp.body; var oidcConf = resp.body;
if (!oidcConf.jwks_uri) { if (!oidcConf.jwks_uri) {
throw new Error("Failed to retrieve openid configuration"); throw new Error("Failed to retrieve openid configuration");
} }
return oidcConf; return oidcConf;
});
});
}; };
keyfetch._wellKnownJwks = function (iss) { keyfetch._wellKnownJwks = async function (iss) {
return Promise.resolve().then(function () {
return keyfetch._jwks(normalizeIss(iss) + "/.well-known/jwks.json"); return keyfetch._jwks(normalizeIss(iss) + "/.well-known/jwks.json");
});
}; };
keyfetch._jwks = function (iss) { keyfetch._jwks = async function (iss) {
return requestAsync({ url: iss, json: true }).then(function (resp) { var resp = await requestAsync({ url: iss, json: true });
return Promise.all( return Promise.all(
resp.body.keys.map(function (jwk) { resp.body.keys.map(async 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) { var thumbprint = await Keypairs.thumbprint({ jwk: jwk });
return Keypairs.export({ jwk: jwk }).then(function (pem) { var pem = await Keypairs.export({ jwk: jwk });
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 = async function (jwkUrl) {
// TODO DRY up a bit // TODO DRY up a bit
return keyfetch._jwks(jwkUrl).then(function (results) { var results = await keyfetch._jwks(jwkUrl);
return Promise.all( await Promise.all(
results.map(function (result) { results.map(async function (result) {
return keyfetch._setCache(result.jwk.iss || jwkUrl, result); return keyfetch._setCache(result.jwk.iss || jwkUrl, result);
}) })
).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));
});
});
}; };
keyfetch.wellKnownJwks = function (iss) { keyfetch.wellKnownJwks = async function (iss) {
// TODO DRY up a bit // TODO DRY up a bit
return keyfetch._wellKnownJwks(iss).then(function (results) { var results = await keyfetch._wellKnownJwks(iss);
return Promise.all( await Promise.all(
results.map(function (result) { results.map(async function (result) {
return keyfetch._setCache(result.jwk.iss || iss, result); return keyfetch._setCache(result.jwk.iss || iss, result);
}) })
).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));
});
});
}; };
keyfetch.oidcJwks = function (iss) { keyfetch.oidcJwks = async function (iss) {
return keyfetch._oidc(iss).then(function (oidcConf) { var oidcConf = await keyfetch._oidc(iss);
// TODO DRY up a bit // TODO DRY up a bit
return keyfetch._jwks(oidcConf.jwks_uri).then(function (results) { var results = await keyfetch._jwks(oidcConf.jwks_uri);
return Promise.all( await Promise.all(
results.map(function (result) { results.map(async function (result) {
return keyfetch._setCache(result.jwk.iss || iss, result); return keyfetch._setCache(result.jwk.iss || iss, result);
}) })
).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));
});
});
});
}; };
function checkId(id) { function checkId(id) {
return function (results) { return function (results) {
@ -125,32 +109,28 @@ function checkId(id) {
return result; return result;
}; };
} }
keyfetch.oidcJwk = function (id, iss) { keyfetch.oidcJwk = async function (id, iss) {
return keyfetch._checkCache(id, iss).then(function (hit) { var hit = await keyfetch._checkCache(id, iss);
if (hit) { if (hit) {
return hit; return hit;
} }
return keyfetch.oidcJwks(iss).then(checkId(id)); return keyfetch.oidcJwks(iss).then(checkId(id));
});
}; };
keyfetch.wellKnownJwk = function (id, iss) { keyfetch.wellKnownJwk = async function (id, iss) {
return keyfetch._checkCache(id, iss).then(function (hit) { var hit = await keyfetch._checkCache(id, iss);
if (hit) { if (hit) {
return hit; return hit;
} }
return keyfetch.wellKnownJwks(iss).then(checkId(id)); return keyfetch.wellKnownJwks(iss).then(checkId(id));
});
}; };
keyfetch.jwk = function (id, jwksUrl) { keyfetch.jwk = async function (id, jwksUrl) {
return keyfetch._checkCache(id, jwksUrl).then(function (hit) { var hit = await keyfetch._checkCache(id, jwksUrl);
if (hit) { if (hit) {
return hit; return hit;
} }
return keyfetch.jwks(jwksUrl).then(checkId(id)); return keyfetch.jwks(jwksUrl).then(checkId(id));
});
}; };
keyfetch._checkCache = function (id, iss) { keyfetch._checkCache = async function (id, iss) {
return Promise.resolve().then(function () {
// We cache by thumbprint and (kid + '@' + iss), // We cache by thumbprint and (kid + '@' + iss),
// so it's safe to check without appending the issuer // so it's safe to check without appending the issuer
var hit = keyCache[id]; var hit = keyCache[id];
@ -172,7 +152,6 @@ keyfetch._checkCache = function (id, iss) {
return JSON.parse(JSON.stringify(hit)); return JSON.parse(JSON.stringify(hit));
} }
return null; return null;
});
}; };
keyfetch._setCache = function (iss, cacheable) { keyfetch._setCache = function (iss, cacheable) {
// force into a number // force into a number
@ -227,17 +206,16 @@ keyfetch.jwt.decode = function (jwt) {
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 = async function (jwt, opts) {
if (!opts) { if (!opts) {
opts = {}; opts = {};
} }
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 err;
if (opts.iss) { if (opts.iss) {
issuers.push(opts.iss); issuers.push(opts.iss);
} }
@ -245,11 +223,9 @@ keyfetch.jwt.verify = function (jwt, opts) {
issuers.push(opts.claims.iss); issuers.push(opts.claims.iss);
} }
if (!issuers.length) { if (!issuers.length) {
err = new Error( throw new Error(
"\n[keyfetch.js] Security Warning:\nNeither of opts.issuers nor opts.iss were provided. The default behavior of setting opts.issuers = ['*'] by default - which allows any or no issuers - is deprecated and will throw an error instead in the next major version.\n" "[keyfetch.js] Security Error: Neither of opts.issuers nor opts.iss were provided. If you would like to bypass issuer verification (i.e. for federated authn) you must explicitly set opts.issuers = ['*']. Otherwise set a value such as https://accounts.google.com/"
); );
console.warn(err);
issuers.push("*");
} }
var claims = opts.claims || {}; var claims = opts.claims || {};
if (!jwt || "string" === typeof jwt) { if (!jwt || "string" === typeof jwt) {
@ -313,13 +289,10 @@ keyfetch.jwt.verify = function (jwt, opts) {
fetchOne = keyfetch.jwk; fetchOne = keyfetch.jwk;
} }
var p;
if (kid) { if (kid) {
p = fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny); return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
} else {
p = fetcher(iss).then(verifyAny);
} }
return p; return fetcher(iss).then(verifyAny);
function verifyOne(hit) { function verifyOne(hit) {
if (true === keyfetch.jws.verify(decoded, hit)) { if (true === keyfetch.jws.verify(decoded, hit)) {
@ -353,17 +326,14 @@ keyfetch.jwt.verify = function (jwt, opts) {
function overrideLookup(jwks) { function overrideLookup(jwks) {
return Promise.all( return Promise.all(
jwks.map(function (jwk) { jwks.map(async function (jwk) {
var Keypairs = jwk.x ? Eckles : Rasha; var Keypairs = jwk.x ? Eckles : Rasha;
return Keypairs.export({ jwk: jwk }).then(function (pem) { var pem = await Keypairs.export({ jwk: jwk });
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { var thumb = await Keypairs.thumbprint({ jwk: jwk });
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) {
@ -380,11 +350,10 @@ keyfetch._decode = function (jwt) {
var obj = keyfetch.jwt.decode(jwt); var obj = keyfetch.jwt.decode(jwt);
return { header: obj.header, payload: obj.claims, signature: obj.signature }; return { header: obj.header, payload: obj.claims, signature: obj.signature };
}; };
keyfetch.verify = function (opts) { keyfetch.verify = async function (opts) {
var jwt = opts.jwt; var jwt = opts.jwt;
return keyfetch.jwt.verify(jwt, opts).then(function (obj) { var obj = await keyfetch.jwt.verify(jwt, opts);
return { header: obj.header, payload: obj.claims, signature: obj.signature }; return { header: obj.header, payload: obj.claims, signature: obj.signature };
});
}; };
function ecdsaJoseSigToAsn1Sig(header, b64sig) { function ecdsaJoseSigToAsn1Sig(header, b64sig) {