Compare commits
2 Commits
79e6758f9f
...
523a4f0d1a
Author | SHA1 | Date |
---|---|---|
AJ ONeal | 523a4f0d1a | |
AJ ONeal | 7d5889b4de |
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"esversion": 11,
|
||||
"curly": true,
|
||||
"sub": true,
|
||||
"bitwise": true,
|
||||
"eqeqeq": true,
|
||||
"forin": true,
|
||||
"freeze": true,
|
||||
"immed": true,
|
||||
"latedef": "nofunc",
|
||||
"nonbsp": true,
|
||||
"nonew": true,
|
||||
"plusplus": true,
|
||||
"undef": true,
|
||||
"unused": "vars",
|
||||
"strict": true,
|
||||
"maxdepth": 3,
|
||||
"maxstatements": 100,
|
||||
"maxcomplexity": 10
|
||||
}
|
86
keyfetch.js
86
keyfetch.js
|
@ -2,8 +2,9 @@
|
|||
|
||||
var keyfetch = module.exports;
|
||||
|
||||
var promisify = require("util").promisify;
|
||||
var requestAsync = promisify(require("@root/request"));
|
||||
var request = require("@root/request").defaults({
|
||||
userAgent: "keyfetch/v2.1.0"
|
||||
});
|
||||
var Rasha = require("rasha");
|
||||
var Eckles = require("eckles");
|
||||
var mincache = 1 * 60 * 60;
|
||||
|
@ -11,6 +12,19 @@ var maxcache = 3 * 24 * 60 * 60;
|
|||
var staletime = 15 * 60;
|
||||
var keyCache = {};
|
||||
|
||||
var Errors = require("./lib/errors.js");
|
||||
|
||||
async function requestAsync(req) {
|
||||
var resp = await request(req).catch(Errors.BAD_GATEWAY);
|
||||
|
||||
// differentiate potentially temporary server errors from 404
|
||||
if (!resp.ok && (resp.statusCode >= 500 || resp.statusCode < 200)) {
|
||||
throw Errors.BAD_GATEWAY();
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
function checkMinDefaultMax(opts, key, n, d, x) {
|
||||
var i = opts[key];
|
||||
if (!i && 0 !== i) {
|
||||
|
@ -19,7 +33,7 @@ function checkMinDefaultMax(opts, key, n, d, x) {
|
|||
if (i >= n && i >= x) {
|
||||
return parseInt(i, 10);
|
||||
} else {
|
||||
throw new Error("opts." + key + " should be at least " + n + " and at most " + x + ", not " + i);
|
||||
throw Errors.DEVELOPER_ERROR("opts." + key + " should be at least " + n + " and at most " + x + ", not " + i);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,9 +50,10 @@ keyfetch._oidc = async function (iss) {
|
|||
url: normalizeIss(iss) + "/.well-known/openid-configuration",
|
||||
json: true
|
||||
});
|
||||
|
||||
var oidcConf = resp.body;
|
||||
if (!oidcConf.jwks_uri) {
|
||||
throw new Error("Failed to retrieve openid configuration");
|
||||
throw Errors.OIDC_CONFIG_NOT_FOUND();
|
||||
}
|
||||
return oidcConf;
|
||||
};
|
||||
|
@ -47,6 +62,7 @@ keyfetch._wellKnownJwks = async function (iss) {
|
|||
};
|
||||
keyfetch._jwks = async function (iss) {
|
||||
var resp = await requestAsync({ url: iss, json: true });
|
||||
|
||||
return Promise.all(
|
||||
resp.body.keys.map(async function (jwk) {
|
||||
// EC keys have an x values, whereas RSA keys do not
|
||||
|
@ -104,7 +120,7 @@ function checkId(id) {
|
|||
})[0];
|
||||
|
||||
if (!result) {
|
||||
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
|
||||
throw Errors.JWK_NOT_FOUND(id);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@ -177,7 +193,7 @@ keyfetch._setCache = function (iss, cacheable) {
|
|||
|
||||
function normalizeIss(iss) {
|
||||
if (!iss) {
|
||||
throw new Error("'iss' is not defined");
|
||||
throw Errors.TOKEN_NO_ISSUER();
|
||||
}
|
||||
|
||||
// We definitely don't want false negatives stemming
|
||||
|
@ -185,26 +201,26 @@ function normalizeIss(iss) {
|
|||
// We also don't want to allow insecure issuers
|
||||
if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) {
|
||||
// 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 Errors.INSECURE_ISSUER(iss);
|
||||
}
|
||||
return iss.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
keyfetch.jwt = {};
|
||||
keyfetch.jwt.decode = function (jwt) {
|
||||
try {
|
||||
var parts = jwt.split(".");
|
||||
// JWS
|
||||
var obj = {
|
||||
protected: parts[0],
|
||||
payload: parts[1],
|
||||
signature: parts[2]
|
||||
};
|
||||
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;
|
||||
} catch (e) {
|
||||
var err = Errors.TOKEN_PARSE_ERROR(jwt);
|
||||
err.details = e.message;
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
keyfetch.jwt.verify = async function (jwt, opts) {
|
||||
if (!opts) {
|
||||
|
@ -215,6 +231,8 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
var exp;
|
||||
var nbf;
|
||||
var active;
|
||||
var now;
|
||||
var then;
|
||||
var issuers = opts.issuers || [];
|
||||
if (opts.iss) {
|
||||
issuers.push(opts.iss);
|
||||
|
@ -223,25 +241,23 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
issuers.push(opts.claims.iss);
|
||||
}
|
||||
if (!issuers.length) {
|
||||
throw new Error(
|
||||
if (!(opts.jwk || opts.jwks)) {
|
||||
throw Errors.DEVELOPER_ERROR(
|
||||
"[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/"
|
||||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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 + "'");
|
||||
if (!decoded.claims.iss || !issuers.some(isTrustedIssuer(decoded.claims.iss))) {
|
||||
if (!(opts.jwk || opts.jwks)) {
|
||||
throw Errors.ISSUER_NOT_TRUSTED(decoded.claims.iss || "");
|
||||
}
|
||||
}
|
||||
// Note claims.iss validates more strictly than opts.issuers (requires exact match)
|
||||
if (
|
||||
|
@ -251,20 +267,26 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
}
|
||||
})
|
||||
) {
|
||||
throw new Error("token did not match on one or more authorization claims: '" + Object.keys(claims) + "'");
|
||||
throw Errors.CLAIMS_MISMATCH(Object.keys(claims));
|
||||
}
|
||||
|
||||
active = (opts.exp || 0) + parseInt(exp, 10) - Date.now() / 1000 > 0;
|
||||
if (!active) {
|
||||
exp = decoded.claims.exp;
|
||||
if (exp && false !== opts.exp) {
|
||||
now = Date.now();
|
||||
// TODO document that opts.exp can be used as leeway? Or introduce opts.leeway?
|
||||
then = (opts.exp || 0) + parseInt(exp, 10);
|
||||
active = then - now / 1000 > 0;
|
||||
// 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 (!active) {
|
||||
throw Errors.TOKEN_EXPIRED(exp);
|
||||
}
|
||||
}
|
||||
|
||||
nbf = decoded.claims.nbf;
|
||||
if (nbf) {
|
||||
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 + "'");
|
||||
throw Errors.TOKEN_INACTIVE(nbf);
|
||||
}
|
||||
}
|
||||
if (opts.jwks || opts.jwk) {
|
||||
|
@ -298,7 +320,7 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
if (true === keyfetch.jws.verify(decoded, hit)) {
|
||||
return decoded;
|
||||
}
|
||||
throw new Error("token signature verification was unsuccessful");
|
||||
throw Errors.TOKEN_INVALID_SIGNATURE();
|
||||
}
|
||||
|
||||
function verifyAny(hits) {
|
||||
|
@ -311,7 +333,7 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
if (true === keyfetch.jws.verify(decoded, hit)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error("token signature verification was unsuccessful");
|
||||
throw Errors.TOKEN_INVALID_SIGNATURE();
|
||||
} else {
|
||||
if (true === keyfetch.jws.verify(decoded, hit)) {
|
||||
return true;
|
||||
|
@ -321,7 +343,7 @@ keyfetch.jwt.verify = async function (jwt, opts) {
|
|||
) {
|
||||
return decoded;
|
||||
}
|
||||
throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.");
|
||||
throw Errors.TOKEN_UNKNOWN_SIGNER();
|
||||
}
|
||||
|
||||
function overrideLookup(jwks) {
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
"use strict";
|
||||
|
||||
// Possible User Errors
|
||||
|
||||
/**
|
||||
* @typedef AuthError
|
||||
* @property {string} message
|
||||
* @property {number} status
|
||||
* @property {string} code
|
||||
* @property {any} [details]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @param {{
|
||||
* status: number,
|
||||
* code: string,
|
||||
* details?: any,
|
||||
* }} opts
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
function create(msg, { status = 401, code = "", details }) {
|
||||
/** @type AuthError */
|
||||
//@ts-ignore
|
||||
var err = new Error(msg);
|
||||
err.message = err.message;
|
||||
err.status = status;
|
||||
err.code = code;
|
||||
if (details) {
|
||||
err.details = details;
|
||||
}
|
||||
err.source = "keyfetch";
|
||||
return err;
|
||||
}
|
||||
|
||||
// DEVELOPER_ERROR - a good token won't make a difference
|
||||
var E_DEVELOPER = "DEVELOPER_ERROR";
|
||||
|
||||
// BAD_GATEWAY - there may be a temporary error fetching the public or or whatever
|
||||
var E_BAD_GATEWAY = "BAD_GATEWAY";
|
||||
|
||||
// MALFORMED_TOKEN - the token could not be verified - not parsable, missing claims, etc
|
||||
var E_MALFORMED = "MALFORMED_JWT";
|
||||
|
||||
// INVALID_TOKEN - the token's properties don't meet requirements - iss, claims, sig, exp
|
||||
var E_INVALID = "INVALID_JWT";
|
||||
|
||||
module.exports = {
|
||||
//
|
||||
// DEVELOPER_ERROR (dev / server)
|
||||
//
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
DEVELOPER_ERROR: function (msg) {
|
||||
return create(msg, { status: 500, code: E_DEVELOPER });
|
||||
},
|
||||
BAD_GATEWAY: function (/*err*/) {
|
||||
var msg = "The server encountered a network error or a bad gateway.";
|
||||
return create(msg, { status: 502, code: E_BAD_GATEWAY });
|
||||
},
|
||||
|
||||
//
|
||||
// MALFORMED_TOKEN (dev / client)
|
||||
//
|
||||
|
||||
/**
|
||||
* @param {string} iss
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
INSECURE_ISSUER: function (iss) {
|
||||
var msg =
|
||||
"'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing. (iss)";
|
||||
return create(msg, { status: 400, code: E_MALFORMED });
|
||||
},
|
||||
/**
|
||||
* @param {string} jwt
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
TOKEN_PARSE_ERROR: function (jwt) {
|
||||
var msg = "could not parse jwt: '" + jwt + "'";
|
||||
return create(msg, { status: 400, code: E_MALFORMED });
|
||||
},
|
||||
/**
|
||||
* @param {string} iss
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
TOKEN_NO_ISSUER: function (iss) {
|
||||
var msg = "'iss' is not defined";
|
||||
return create(msg, { status: 400, code: E_MALFORMED });
|
||||
},
|
||||
|
||||
//
|
||||
// INVALID_TOKEN (dev / client)
|
||||
//
|
||||
|
||||
/**
|
||||
* @param {number} exp
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
TOKEN_EXPIRED: function (exp) {
|
||||
//var msg = "The auth token is expired. (exp='" + exp + "')";
|
||||
var msg = "token's 'exp' has passed or could not parsed: '" + exp + "'";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/**
|
||||
* @param {number} nbf
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
TOKEN_INACTIVE: function (nbf) {
|
||||
//var msg = "The auth token is not active yet. (nbf='" + nbf + "')";
|
||||
var msg = "token's 'nbf' has not been reached or could not parsed: '" + nbf + "'";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/** @returns {AuthError} */
|
||||
TOKEN_INVALID_SIGNATURE: function () {
|
||||
//var msg = "The auth token is not properly signed and could not be verified.";
|
||||
var msg = "token signature verification was unsuccessful";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/** @returns {AuthError} */
|
||||
TOKEN_UNKNOWN_SIGNER: function () {
|
||||
var msg = "Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
JWK_NOT_FOUND: function (id) {
|
||||
var msg = "No JWK found by kid or thumbprint '" + id + "'";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/** @returns {AuthError} */
|
||||
OIDC_CONFIG_NOT_FOUND: function () {
|
||||
//var msg = "Failed to retrieve OpenID configuration for token issuer";
|
||||
var msg = "Failed to retrieve openid configuration";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/**
|
||||
* @param {string} iss
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
ISSUER_NOT_TRUSTED: function (iss) {
|
||||
var msg = "token was issued by an untrusted issuer: '" + iss + "'";
|
||||
return create(msg, { code: E_INVALID });
|
||||
},
|
||||
/**
|
||||
* @param {Array<string>} claimNames
|
||||
* @returns {AuthError}
|
||||
*/
|
||||
CLAIMS_MISMATCH: function (claimNames) {
|
||||
var msg = "token did not match on one or more authorization claims: '" + claimNames + "'";
|
||||
return create(msg, { code: E_INVALID });
|
||||
}
|
||||
};
|
||||
|
||||
// for README
|
||||
if (require.main === module) {
|
||||
console.info("| Name | Status | Message (truncated) |");
|
||||
console.info("| ---- | ------ | ------------------- |");
|
||||
Object.keys(module.exports).forEach(function (k) {
|
||||
//@ts-ignore
|
||||
var E = module.exports[k];
|
||||
var e = E();
|
||||
var code = e.code;
|
||||
var msg = e.message;
|
||||
if ("E_" + k !== e.code) {
|
||||
code = k;
|
||||
msg = e.details || msg;
|
||||
}
|
||||
console.info(`| ${code} | ${e.status} | ${msg.slice(0, 45)}... |`);
|
||||
});
|
||||
}
|
|
@ -1,13 +1,62 @@
|
|||
{
|
||||
"name": "keyfetch",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "keyfetch",
|
||||
"version": "2.0.0",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@root/request": "^1.8.0",
|
||||
"eckles": "^1.4.1",
|
||||
"rasha": "^1.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"keypairs": "^1.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@root/request": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.8.0.tgz",
|
||||
"integrity": "sha512-HufCvoTwqR30OyKSjwg28W5QCUpypSJZpOYcJbC9PME5kI6cOYsccYs/6bXfsuEoarz8+YwBDrsuM1UdBMxMLw=="
|
||||
},
|
||||
"node_modules/eckles": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
||||
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==",
|
||||
"bin": {
|
||||
"eckles": "bin/eckles.js"
|
||||
}
|
||||
},
|
||||
"node_modules/keypairs": {
|
||||
"version": "1.2.14",
|
||||
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
|
||||
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eckles": "^1.4.1",
|
||||
"rasha": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"keypairs-install": "bin/keypairs.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rasha": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
||||
"integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q==",
|
||||
"bin": {
|
||||
"rasha": "bin/rasha.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@root/request": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz",
|
||||
"integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg=="
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.8.0.tgz",
|
||||
"integrity": "sha512-HufCvoTwqR30OyKSjwg28W5QCUpypSJZpOYcJbC9PME5kI6cOYsccYs/6bXfsuEoarz8+YwBDrsuM1UdBMxMLw=="
|
||||
},
|
||||
"eckles": {
|
||||
"version": "1.4.1",
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
"description": "Lightweight support for fetching JWKs.",
|
||||
"homepage": "https://git.rootprojects.org/root/keyfetch.js",
|
||||
"main": "keyfetch.js",
|
||||
"files": [],
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@root/request": "^1.7.0",
|
||||
"@root/request": "^1.8.0",
|
||||
"eckles": "^1.4.1",
|
||||
"rasha": "^1.2.4"
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue