security: obey opts.iss, issue warning by default
This commit is contained in:
parent
9141962456
commit
8ec2d98645
13
README.md
13
README.md
|
@ -1,4 +1,4 @@
|
||||||
# keyfetch
|
# [keyfetch](https://git.rootprojects.org/root/keyfetch.js)
|
||||||
|
|
||||||
Lightweight support for fetching JWKs.
|
Lightweight support for fetching JWKs.
|
||||||
|
|
||||||
|
@ -178,15 +178,16 @@ If your authorization `claims` can be expressed as exact string matches, you can
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.jwt.verify(jwt, {
|
keyfetch.jwt.verify(jwt, {
|
||||||
strategy: 'oidc'
|
strategy: 'oidc',
|
||||||
, issuers: [ 'https://example.com/' ]
|
issuers: [ 'https://example.com/' ],
|
||||||
, claims: { role: 'admin', sub: 'abc', group: 'xyz' }
|
//iss: 'https://example.com/',
|
||||||
|
claims: { role: 'admin', sub: 'abc', group: 'xyz' }
|
||||||
}).then(function (verified) {
|
}).then(function (verified) {
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- `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), or '\*'
|
||||||
|
- `iss` is like `issuers`, but only one
|
||||||
- `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)
|
||||||
|
|
|
@ -25,29 +25,32 @@ keyfetch
|
||||||
});
|
});
|
||||||
|
|
||||||
/*global Promise*/
|
/*global Promise*/
|
||||||
var keypairs = require("keypairs.js");
|
var keypairs = require("keypairs");
|
||||||
keypairs.generate().then(function (pair) {
|
keypairs.generate().then(function (pair) {
|
||||||
|
var iss = "https://example.com/";
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
keypairs
|
keypairs
|
||||||
.signJwt({
|
.signJwt({
|
||||||
jwk: pair.private,
|
jwk: pair.private,
|
||||||
iss: "https://example.com/",
|
iss: iss,
|
||||||
sub: "mikey",
|
sub: "mikey",
|
||||||
exp: "1h"
|
exp: "1h"
|
||||||
})
|
})
|
||||||
.then(function (jwt) {
|
.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, iss: "*" }).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
|
||||||
if (!(verified.claims && verified.claims.exp)) {
|
.verify(keyfetch.jwt.decode(jwt), { jwk: pair.public, iss: iss })
|
||||||
throw new Error("malformed decoded token");
|
.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, { jwks: [pair.public], issuers: [iss] }),
|
||||||
keyfetch.jwt.verify(jwt, {
|
keyfetch.jwt.verify(jwt, {
|
||||||
jwk: pair.public,
|
jwk: pair.public,
|
||||||
issuers: ["https://example.com/"]
|
issuers: ["https://example.com/"]
|
||||||
|
@ -117,8 +120,18 @@ 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([
|
||||||
keyfetch.jwt.verify(jwt, { jwk: pair.public }),
|
// test that the old behavior of defaulting to '*' still works
|
||||||
|
keyfetch.jwt.verify(jwt, { jwk: pair.public }).then(function () {
|
||||||
|
if (!warned) {
|
||||||
|
throw e("should have issued security warning about allow all by default");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
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
|
||||||
.verify(jwt, {
|
.verify(jwt, {
|
||||||
|
|
24
keyfetch.js
24
keyfetch.js
|
@ -3,7 +3,7 @@
|
||||||
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("@root/request"));
|
||||||
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;
|
||||||
|
@ -197,6 +197,10 @@ keyfetch._setCache = function (iss, cacheable) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeIss(iss) {
|
function normalizeIss(iss) {
|
||||||
|
if (!iss) {
|
||||||
|
throw new Error("'iss' is not defined");
|
||||||
|
}
|
||||||
|
|
||||||
// We definitely don't want false negatives stemming
|
// We definitely don't want false negatives stemming
|
||||||
// from https://example.com vs https://example.com/
|
// from https://example.com vs https://example.com/
|
||||||
// We also don't want to allow insecure issuers
|
// We also don't want to allow insecure issuers
|
||||||
|
@ -232,7 +236,21 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
||||||
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) {
|
||||||
|
issuers.push(opts.iss);
|
||||||
|
}
|
||||||
|
if (opts.claims && opts.claims.iss) {
|
||||||
|
issuers.push(opts.claims.iss);
|
||||||
|
}
|
||||||
|
if (!issuers.length) {
|
||||||
|
err = 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"
|
||||||
|
);
|
||||||
|
console.warn(err);
|
||||||
|
issuers.push("*");
|
||||||
|
}
|
||||||
var claims = opts.claims || {};
|
var claims = opts.claims || {};
|
||||||
if (!jwt || "string" === typeof jwt) {
|
if (!jwt || "string" === typeof jwt) {
|
||||||
try {
|
try {
|
||||||
|
@ -249,7 +267,7 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
||||||
if (!issuers.some(isTrustedIssuer(decoded.claims.iss))) {
|
if (!issuers.some(isTrustedIssuer(decoded.claims.iss))) {
|
||||||
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?
|
// Note claims.iss validates more strictly than opts.issuers (requires exact match)
|
||||||
if (
|
if (
|
||||||
!Object.keys(claims).every(function (key) {
|
!Object.keys(claims).every(function (key) {
|
||||||
if (claims[key] === decoded.claims[key]) {
|
if (claims[key] === decoded.claims[key]) {
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
{
|
{
|
||||||
"name": "keyfetch",
|
"name": "keyfetch",
|
||||||
"version": "1.1.8",
|
"version": "1.2.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coolaj86/urequest": {
|
"@root/request": {
|
||||||
"version": "1.3.7",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz",
|
||||||
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA=="
|
"integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg=="
|
||||||
},
|
},
|
||||||
"eckles": {
|
"eckles": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
||||||
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
||||||
},
|
},
|
||||||
|
"keypairs": {
|
||||||
|
"version": "1.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
|
||||||
|
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"eckles": "^1.4.1",
|
||||||
|
"rasha": "^1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rasha": {
|
"rasha": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
||||||
|
|
69
package.json
69
package.json
|
@ -1,36 +1,37 @@
|
||||||
{
|
{
|
||||||
"name": "keyfetch",
|
"name": "keyfetch",
|
||||||
"version": "1.2.1",
|
"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.rootprojects.org/root/keyfetch.js",
|
||||||
"main": "keyfetch.js",
|
"main": "keyfetch.js",
|
||||||
"files": [],
|
"files": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coolaj86/urequest": "^1.3.7",
|
"@root/request": "^1.7.0",
|
||||||
"eckles": "^1.4.1",
|
"eckles": "^1.4.1",
|
||||||
"rasha": "^1.2.4"
|
"rasha": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {
|
||||||
"scripts": {
|
"keypairs": "^1.2.14"
|
||||||
"test": "node keyfetch-test.js"
|
},
|
||||||
},
|
"scripts": {
|
||||||
"repository": {
|
"test": "node keyfetch-test.js"
|
||||||
"type": "git",
|
},
|
||||||
"url": "https://git.coolaj86.com/coolaj86/keyfetch.js.git"
|
"repository": {
|
||||||
},
|
"type": "git",
|
||||||
"keywords": [
|
"url": "https://git.rootprojects.org/root/keyfetch.js.git"
|
||||||
"jwks",
|
},
|
||||||
"jwk",
|
"keywords": [
|
||||||
"jwt",
|
"jwks",
|
||||||
"auth0",
|
"jwk",
|
||||||
"pem",
|
"jwt",
|
||||||
"RSA",
|
"auth0",
|
||||||
"EC",
|
"pem",
|
||||||
"ECDSA",
|
"RSA",
|
||||||
"OIDC",
|
"EC",
|
||||||
"well-known"
|
"ECDSA",
|
||||||
],
|
"OIDC",
|
||||||
"author": "AJ ONeal <solderjs@gmail.com> (https://coolaj86.com/)",
|
"well-known"
|
||||||
"license": "MPL-2.0"
|
],
|
||||||
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
|
"license": "MPL-2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue