make Prettier
This commit is contained in:
parent
a32b942861
commit
aba70bf0ff
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
90
README.md
90
README.md
|
@ -9,17 +9,17 @@ Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `j
|
|||
|
||||
Works great for
|
||||
|
||||
* [x] `jsonwebtoken` (Auth0)
|
||||
* [x] OIDC (OpenID Connect)
|
||||
* [x] .well-known/jwks.json (Auth0)
|
||||
* [x] Other JWKs URLs
|
||||
- [x] `jsonwebtoken` (Auth0)
|
||||
- [x] OIDC (OpenID Connect)
|
||||
- [x] .well-known/jwks.json (Auth0)
|
||||
- [x] Other JWKs URLs
|
||||
|
||||
Crypto Support
|
||||
|
||||
* [x] JWT verification
|
||||
* [x] RSA (all variants)
|
||||
* [x] EC / ECDSA (NIST variants P-256, P-384)
|
||||
* [ ] esoteric variants (excluded to keep the code featherweight and secure)
|
||||
- [x] JWT verification
|
||||
- [x] RSA (all variants)
|
||||
- [x] EC / ECDSA (NIST variants P-256, P-384)
|
||||
- [ ] esoteric variants (excluded to keep the code featherweight and secure)
|
||||
|
||||
# Install
|
||||
|
||||
|
@ -32,7 +32,7 @@ npm install --save keyfetch
|
|||
Retrieve a key list of keys:
|
||||
|
||||
```js
|
||||
var keyfetch = require('keyfetch');
|
||||
var keyfetch = require("keyfetch");
|
||||
|
||||
keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
||||
results.forEach(function (result) {
|
||||
|
@ -46,8 +46,8 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
|||
Quick JWT verification (for authentication):
|
||||
|
||||
```js
|
||||
var keyfetch = require('keyfetch');
|
||||
var jwt = '...';
|
||||
var keyfetch = require("keyfetch");
|
||||
var jwt = "...";
|
||||
|
||||
keyfetch.jwt.verify(jwt).then(function (decoded) {
|
||||
console.log(decoded);
|
||||
|
@ -57,7 +57,7 @@ keyfetch.jwt.verify(jwt).then(function (decoded) {
|
|||
JWT verification (for authorization):
|
||||
|
||||
```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) {
|
||||
console.log(decoded);
|
||||
});
|
||||
|
@ -66,19 +66,16 @@ keyfetch.jwt.verify(jwt, options).then(function (decoded) {
|
|||
Verify a JWT with `jsonwebtoken`:
|
||||
|
||||
```js
|
||||
var keyfetch = require('keyfetch');
|
||||
var jwt = require('jsonwebtoken');
|
||||
var keyfetch = require("keyfetch");
|
||||
var jwt = require("jsonwebtoken");
|
||||
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)) {
|
||||
throw new Error("untrusted issuer");
|
||||
}
|
||||
|
||||
keyfetch.oidcJwk(
|
||||
token.header.kid
|
||||
, token.payload.iss
|
||||
).then(function (result) {
|
||||
keyfetch.oidcJwk(token.header.kid, token.payload.iss).then(function (result) {
|
||||
console.log(result.jwk);
|
||||
console.log(result.thumprint);
|
||||
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
|
||||
function isTrustedIssuer(iss) {
|
||||
return -1 !== [ 'https://partner.com/', 'https://auth0.com/'].indexOf(iss);
|
||||
return -1 !== ["https://partner.com/", "https://auth0.com/"].indexOf(iss);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
function isTrustedIssuer(iss) {
|
||||
return /^https:/.test(iss) && // must be a secure domain
|
||||
/(\.|^)example\.com$/.test(iss); // can be example.com or any subdomain
|
||||
return (
|
||||
/^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).
|
||||
|
||||
```js
|
||||
keyfetch.jwks(jwksUrl)
|
||||
keyfetch.jwks(jwksUrl);
|
||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||
```
|
||||
|
||||
```js
|
||||
keyfetch.jwk(id, jwksUrl)
|
||||
keyfetch.jwk(id, jwksUrl);
|
||||
// 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.
|
||||
|
||||
```js
|
||||
keyfetch.wellKnownJwks(issuerUrl)
|
||||
keyfetch.wellKnownJwks(issuerUrl);
|
||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||
```
|
||||
|
||||
```js
|
||||
keyfetch.wellKnownJwk(id, issuerUrl)
|
||||
keyfetch.wellKnownJwk(id, issuerUrl);
|
||||
// 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.
|
||||
|
||||
```js
|
||||
keyfetch.oidcJwks(issuerUrl)
|
||||
keyfetch.oidcJwks(issuerUrl);
|
||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||
```
|
||||
|
||||
```js
|
||||
keyfetch.oidcJwk(id, issuerUrl)
|
||||
keyfetch.oidcJwk(id, issuerUrl);
|
||||
// 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.
|
||||
|
||||
```js
|
||||
keyfetch.jwt.verify(jwt, { strategy: 'oidc' }).then(function (verified) {
|
||||
keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
|
||||
/*
|
||||
{ protected: '...' // base64 header
|
||||
, payload: '...' // base64 payload
|
||||
|
@ -187,12 +185,12 @@ keyfetch.jwt.verify(jwt, {
|
|||
|
||||
```
|
||||
|
||||
* `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
|
||||
- `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
|
||||
|
||||
|
@ -217,16 +215,16 @@ 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 parts = jwt.split(".");
|
||||
var obj = {
|
||||
protected: parts[0]
|
||||
, payload: parts[1]
|
||||
, signature: parts[2]
|
||||
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'));
|
||||
obj.header = JSON.parse(Buffer.from(obj.protected, "base64"));
|
||||
obj.claims = JSON.parse(Buffer.from(obj.payload, "base64"));
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
@ -237,14 +235,14 @@ keyfetch.jwt.decode = function (jwt) {
|
|||
```js
|
||||
keyfetch.init({
|
||||
// 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)
|
||||
, maxcache: 3 * 24 * 60 * 60
|
||||
maxcache: 3 * 24 * 60 * 60,
|
||||
|
||||
// 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.
|
||||
|
|
145
keyfetch-test.js
145
keyfetch-test.js
|
@ -1,10 +1,12 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
var keyfetch = require('./keyfetch.js');
|
||||
var keyfetch = require("./keyfetch.js");
|
||||
var testIss = "https://example.auth0.com";
|
||||
|
||||
keyfetch.init({});
|
||||
keyfetch.oidcJwks(testIss).then(function (hits) {
|
||||
keyfetch
|
||||
.oidcJwks(testIss)
|
||||
.then(function (hits) {
|
||||
keyfetch._clear();
|
||||
//console.log(hits);
|
||||
return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () {
|
||||
|
@ -12,65 +14,126 @@ keyfetch.oidcJwks(testIss).then(function (hits) {
|
|||
//console.log(jwk);
|
||||
});
|
||||
});
|
||||
}).then(function () {
|
||||
})
|
||||
.then(function () {
|
||||
console.log("Fetching PASSES");
|
||||
}).catch(function (err) {
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("NONE SHALL PASS!");
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
/*global Promise*/
|
||||
var keypairs = require('keypairs.js');
|
||||
var keypairs = require("keypairs.js");
|
||||
keypairs.generate().then(function (pair) {
|
||||
return Promise.all([
|
||||
keypairs.signJwt({
|
||||
jwk: pair.private, iss: 'https://example.com/', sub: 'mikey', exp: '1h'
|
||||
}).then(function (jwt) {
|
||||
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) {
|
||||
}),
|
||||
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: ["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] })
|
||||
, 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'] })
|
||||
.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(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)
|
||||
]);
|
||||
}),
|
||||
keypairs
|
||||
.signJwt({
|
||||
jwk: pair.private,
|
||||
iss: false,
|
||||
sub: "mikey",
|
||||
exp: "1h"
|
||||
})
|
||||
, keypairs.signJwt({
|
||||
jwk: pair.private, iss: false, sub: 'mikey', exp: '1h'
|
||||
}).then(function (jwt) {
|
||||
.then(function (jwt) {
|
||||
return Promise.all([
|
||||
keyfetch.jwt.verify(jwt, { jwk: pair.public })
|
||||
, keyfetch.jwt.verify(jwt)
|
||||
.then(e("should have an issuer")).catch(throwIfNotExpected)
|
||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com/'] })
|
||||
.then(e("fail when issuer specified and doesn't exist")).catch(throwIfNotExpected)
|
||||
keyfetch.jwt.verify(jwt, { jwk: pair.public }),
|
||||
keyfetch.jwt.verify(jwt).then(e("should have an issuer")).catch(throwIfNotExpected),
|
||||
keyfetch.jwt
|
||||
.verify(jwt, {
|
||||
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");
|
||||
}).catch(function (err) {
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("NONE SHALL PASS!");
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
@ -87,5 +150,7 @@ function e(msg) {
|
|||
return new Error("ETEST: " + msg);
|
||||
}
|
||||
function throwIfNotExpected(err) {
|
||||
if ("ETEST" === err.message.slice(0, 5)) { throw err; }
|
||||
if ("ETEST" === err.message.slice(0, 5)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
218
keyfetch.js
218
keyfetch.js
|
@ -1,11 +1,11 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
var keyfetch = module.exports;
|
||||
|
||||
var promisify = require('util').promisify;
|
||||
var requestAsync = promisify(require('@coolaj86/urequest'));
|
||||
var Rasha = require('rasha');
|
||||
var Eckles = require('eckles');
|
||||
var promisify = require("util").promisify;
|
||||
var requestAsync = promisify(require("@coolaj86/urequest"));
|
||||
var Rasha = require("rasha");
|
||||
var Eckles = require("eckles");
|
||||
var mincache = 1 * 60 * 60;
|
||||
var maxcache = 3 * 24 * 60 * 60;
|
||||
var staletime = 15 * 60;
|
||||
|
@ -14,7 +14,9 @@ var keyCache = {};
|
|||
/*global Promise*/
|
||||
function checkMinDefaultMax(opts, key, n, d, x) {
|
||||
var i = opts[key];
|
||||
if (!i && 0 !== i) { return d; }
|
||||
if (!i && 0 !== i) {
|
||||
return d;
|
||||
}
|
||||
if (i >= n && i >= x) {
|
||||
return parseInt(i, 10);
|
||||
} else {
|
||||
|
@ -26,27 +28,15 @@ keyfetch._clear = function () {
|
|||
keyCache = {};
|
||||
};
|
||||
keyfetch.init = function (opts) {
|
||||
mincache = checkMinDefaultMax(opts, 'mincache',
|
||||
1 * 60,
|
||||
mincache,
|
||||
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
|
||||
);
|
||||
mincache = checkMinDefaultMax(opts, "mincache", 1 * 60, mincache, 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) {
|
||||
return Promise.resolve().then(function () {
|
||||
return requestAsync({
|
||||
url: normalizeIss(iss) + '/.well-known/openid-configuration'
|
||||
, json: true
|
||||
url: normalizeIss(iss) + "/.well-known/openid-configuration",
|
||||
json: true
|
||||
}).then(function (resp) {
|
||||
var oidcConf = resp.body;
|
||||
if (!oidcConf.jwks_uri) {
|
||||
|
@ -58,33 +48,37 @@ keyfetch._oidc = function (iss) {
|
|||
};
|
||||
keyfetch._wellKnownJwks = 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) {
|
||||
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
|
||||
var Keypairs = jwk.x ? Eckles : Rasha;
|
||||
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) {
|
||||
return Keypairs.export({ jwk: jwk }).then(function (pem) {
|
||||
var cacheable = {
|
||||
jwk: jwk
|
||||
, thumbprint: thumbprint
|
||||
, pem: pem
|
||||
jwk: jwk,
|
||||
thumbprint: thumbprint,
|
||||
pem: pem
|
||||
};
|
||||
return cacheable;
|
||||
});
|
||||
});
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
keyfetch.jwks = function (jwkUrl) {
|
||||
// TODO DRY up a bit
|
||||
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);
|
||||
})).then(function () {
|
||||
})
|
||||
).then(function () {
|
||||
// cacheable -> hit (keep original externally immutable)
|
||||
return JSON.parse(JSON.stringify(results));
|
||||
});
|
||||
|
@ -93,9 +87,11 @@ keyfetch.jwks = function (jwkUrl) {
|
|||
keyfetch.wellKnownJwks = function (iss) {
|
||||
// TODO DRY up a bit
|
||||
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);
|
||||
})).then(function () {
|
||||
})
|
||||
).then(function () {
|
||||
// result -> hit (keep original externally immutable)
|
||||
return JSON.parse(JSON.stringify(results));
|
||||
});
|
||||
|
@ -105,9 +101,11 @@ keyfetch.oidcJwks = function (iss) {
|
|||
return keyfetch._oidc(iss).then(function (oidcConf) {
|
||||
// TODO DRY up a bit
|
||||
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);
|
||||
})).then(function () {
|
||||
})
|
||||
).then(function () {
|
||||
// result -> hit (keep original externally immutable)
|
||||
return JSON.parse(JSON.stringify(results));
|
||||
});
|
||||
|
@ -129,19 +127,25 @@ function checkId(id) {
|
|||
}
|
||||
keyfetch.oidcJwk = function (id, iss) {
|
||||
return keyfetch._checkCache(id, iss).then(function (hit) {
|
||||
if (hit) { return hit; }
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
return keyfetch.oidcJwks(iss).then(checkId(id));
|
||||
});
|
||||
};
|
||||
keyfetch.wellKnownJwk = function (id, iss) {
|
||||
return keyfetch._checkCache(id, iss).then(function (hit) {
|
||||
if (hit) { return hit; }
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
return keyfetch.wellKnownJwks(iss).then(checkId(id));
|
||||
});
|
||||
};
|
||||
keyfetch.jwk = function (id, jwksUrl) {
|
||||
return keyfetch._checkCache(id, jwksUrl).then(function (hit) {
|
||||
if (hit) { return hit; }
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
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
|
||||
var hit = keyCache[id];
|
||||
if (!hit) {
|
||||
hit = keyCache[id + '@' + normalizeIss(iss)];
|
||||
hit = keyCache[id + "@" + normalizeIss(iss)];
|
||||
}
|
||||
if (!hit) {
|
||||
return null;
|
||||
|
@ -189,7 +193,7 @@ keyfetch._setCache = function (iss, cacheable) {
|
|||
cacheable.createdAt = now;
|
||||
cacheable.expiresAt = expiresAt;
|
||||
keyCache[cacheable.thumbprint] = cacheable;
|
||||
keyCache[cacheable.jwk.kid + '@' + normalizeIss(iss)] = cacheable;
|
||||
keyCache[cacheable.jwk.kid + "@" + normalizeIss(iss)] = cacheable;
|
||||
};
|
||||
|
||||
function normalizeIss(iss) {
|
||||
|
@ -198,37 +202,44 @@ 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 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.decode = function (jwt) {
|
||||
var parts = jwt.split('.');
|
||||
var parts = jwt.split(".");
|
||||
// JWS
|
||||
var obj = {
|
||||
protected: parts[0]
|
||||
, payload: parts[1]
|
||||
, signature: parts[2]
|
||||
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'));
|
||||
obj.header = JSON.parse(Buffer.from(obj.protected, "base64"));
|
||||
obj.claims = JSON.parse(Buffer.from(obj.payload, "base64"));
|
||||
return obj;
|
||||
};
|
||||
keyfetch.jwt.verify = function (jwt, opts) {
|
||||
if (!opts) { opts = {}; }
|
||||
if (!opts) {
|
||||
opts = {};
|
||||
}
|
||||
return Promise.resolve().then(function () {
|
||||
var decoded;
|
||||
var exp;
|
||||
var nbf;
|
||||
var active;
|
||||
var issuers = opts.issuers || ['*'];
|
||||
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 + "'"); }
|
||||
if (!jwt || "string" === typeof jwt) {
|
||||
try {
|
||||
decoded = keyfetch.jwt.decode(jwt);
|
||||
} catch (e) {
|
||||
throw new Error("could not parse jwt: '" + jwt + "'");
|
||||
}
|
||||
} else {
|
||||
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 + "'");
|
||||
}
|
||||
// TODO verify claims also?
|
||||
if (!Object.keys(claims).every(function (key) {
|
||||
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);
|
||||
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) {
|
||||
|
@ -255,7 +268,7 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
|||
}
|
||||
}
|
||||
if (nbf) {
|
||||
active = (parseInt(nbf, 10) - (Date.now()/1000) <= 0);
|
||||
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 + "'");
|
||||
}
|
||||
|
@ -268,11 +281,11 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
|||
var iss;
|
||||
var fetcher;
|
||||
var fetchOne;
|
||||
if (!opts.strategy || 'oidc' === opts.strategy) {
|
||||
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) {
|
||||
} else if ("auth0" === opts.strategy || "well-known" === opts.strategy) {
|
||||
iss = decoded.claims.iss;
|
||||
fetcher = keyfetch.wellKnownJwks;
|
||||
fetchOne = keyfetch.wellKnownJwk;
|
||||
|
@ -294,45 +307,54 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
|||
if (true === keyfetch.jws.verify(decoded, hit)) {
|
||||
return decoded;
|
||||
}
|
||||
throw new Error('token signature verification was unsuccessful');
|
||||
throw new Error("token signature verification was unsuccessful");
|
||||
}
|
||||
|
||||
function verifyAny(hits) {
|
||||
if (hits.some(function (hit) {
|
||||
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; }
|
||||
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) {
|
||||
return Promise.all(
|
||||
jwks.map(function (jwk) {
|
||||
var Keypairs = jwk.x ? Eckles : Rasha;
|
||||
return Keypairs.export({ jwk: jwk }).then(function (pem) {
|
||||
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
|
||||
return { jwk: jwk, pem: pem, thumbprint: thumb };
|
||||
});
|
||||
});
|
||||
})).then(verifyAny);
|
||||
})
|
||||
).then(verifyAny);
|
||||
}
|
||||
});
|
||||
};
|
||||
keyfetch.jws = {};
|
||||
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);
|
||||
return require('crypto')
|
||||
return require("crypto")
|
||||
.createVerify(alg)
|
||||
.update(jws.protected + '.' + jws.payload)
|
||||
.verify(pub.pem, sig, 'base64')
|
||||
;
|
||||
.update(jws.protected + "." + jws.payload)
|
||||
.verify(pub.pem, sig, "base64");
|
||||
};
|
||||
|
||||
// old, gotta make sure nothing else uses this
|
||||
|
@ -350,44 +372,56 @@ keyfetch.verify = function (opts) {
|
|||
function ecdsaJoseSigToAsn1Sig(header, b64sig) {
|
||||
// ECDSA JWT signatures differ from "normal" ECDSA signatures
|
||||
// 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 r = bufsig.slice(0, hlen);
|
||||
var s = bufsig.slice(hlen);
|
||||
// unpad positive ints less than 32 bytes wide
|
||||
while (!r[0]) { r = r.slice(1); }
|
||||
while (!s[0]) { s = s.slice(1); }
|
||||
while (!r[0]) {
|
||||
r = r.slice(1);
|
||||
}
|
||||
while (!s[0]) {
|
||||
s = s.slice(1);
|
||||
}
|
||||
// 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 & s[0]) { s = Buffer.concat([Buffer.from([0]), s]); }
|
||||
if (0x80 & r[0]) {
|
||||
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 head = [0x30];
|
||||
// hard code 0x80 + 1 because it won't be longer than
|
||||
// two SHA512 plus two pad bytes (130 bytes <= 256)
|
||||
if (len >= 0x80) { head.push(0x81); }
|
||||
if (len >= 0x80) {
|
||||
head.push(0x81);
|
||||
}
|
||||
head.push(len);
|
||||
|
||||
var buf = Buffer.concat([
|
||||
Buffer.from(head)
|
||||
, Buffer.from([0x02, r.byteLength]), r
|
||||
, Buffer.from([0x02, s.byteLength]), s
|
||||
Buffer.from(head),
|
||||
Buffer.from([0x02, r.byteLength]),
|
||||
r,
|
||||
Buffer.from([0x02, s.byteLength]),
|
||||
s
|
||||
]);
|
||||
|
||||
return buf.toString('base64')
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
.replace(/=/g, '')
|
||||
;
|
||||
return buf.toString("base64").replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
||||
}
|
||||
|
||||
function isTrustedIssuer(issuer) {
|
||||
return function (trusted) {
|
||||
if ('*' === trusted) { return true; }
|
||||
if ("*" === trusted) {
|
||||
return true;
|
||||
}
|
||||
// TODO account for '*.example.com'
|
||||
trusted = (/^http(s?):\/\//.test(trusted) ? trusted : ('https://' + trusted));
|
||||
return issuer.replace(/\/$/, '') === trusted.replace(/\/$/, '') && trusted;
|
||||
trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted;
|
||||
return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue