initial commit
This commit is contained in:
commit
6c071af9cd
|
@ -0,0 +1,173 @@
|
||||||
|
# keyfetch
|
||||||
|
|
||||||
|
Lightweight support for fetching JWKs.
|
||||||
|
|
||||||
|
Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `jsonwebtoken` package
|
||||||
|
(and node's native RSA and ECDSA crypto APIs).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
Works great for
|
||||||
|
|
||||||
|
* [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)
|
||||||
|
|
||||||
|
# Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save keyfetch
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
Retrieve a key list of keys:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var keyfetch = require('keyfetch');
|
||||||
|
|
||||||
|
keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
||||||
|
results.forEach(function (result) {
|
||||||
|
console.log(result.jwk);
|
||||||
|
console.log(result.thumprint);
|
||||||
|
console.log(result.pem);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Quick JWT verification:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var keyfetch = require('keyfetch');
|
||||||
|
var jwt = '...';
|
||||||
|
|
||||||
|
keyfetch.verify({ jwt: jwt }).then(function (decoded) {
|
||||||
|
console.log(decoded);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify a JWT with `jsonwebtoken`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var keyfetch = require('keyfetch');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
|
var auth = "..."; // some JWT
|
||||||
|
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) {
|
||||||
|
console.log(result.jwk);
|
||||||
|
console.log(result.thumprint);
|
||||||
|
console.log(result.pem);
|
||||||
|
|
||||||
|
jwt.verify(jwt, pem);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note*: You might implement `isTrustedIssuer` one of these:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function isTrustedIssuer(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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# API
|
||||||
|
|
||||||
|
All API calls will return the RFC standard JWK SHA256 thumbprint as well as a PEM version of the key.
|
||||||
|
|
||||||
|
Note: When specifying `id`, it may be either `kid` (as in `token.header.kid`)
|
||||||
|
or `thumbprint` (as in `result.thumbprint`).
|
||||||
|
|
||||||
|
### JWKs URLs
|
||||||
|
|
||||||
|
Retrieves keys from a URL such as `https://example.com/jwks/` with the format `{ keys: [ { kid, kty, exp, ... } ] }`
|
||||||
|
and returns the array of keys (as well as thumbprint and jwk-to-pem).
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.jwks(jwksUrl)
|
||||||
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.jwk(id, jwksUrl)
|
||||||
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth0
|
||||||
|
|
||||||
|
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)
|
||||||
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.wellKnownJwk(id, issuerUrl)
|
||||||
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
### OIDC
|
||||||
|
|
||||||
|
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)
|
||||||
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.oidcJwk(id, issuerUrl)
|
||||||
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify JWT
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.verify({ jwt: jwk, strategy: 'oidc' })
|
||||||
|
// Promises a decoded JWT { headers, payload, signature } or fails
|
||||||
|
```
|
||||||
|
|
||||||
|
* `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url.
|
||||||
|
|
||||||
|
### Cache Settings
|
||||||
|
|
||||||
|
```js
|
||||||
|
keyfetch.init({
|
||||||
|
// set all keys at least 1 hour (regardless of jwk.exp)
|
||||||
|
mincache: 1 * 60 * 60
|
||||||
|
|
||||||
|
// expire each key after 3 days (regardless of jwk.exp)
|
||||||
|
, maxcache: 3 * 24 * 60 * 60
|
||||||
|
|
||||||
|
// re-fetch a key up to 15 minutes before it expires (only if used)
|
||||||
|
, staletime: 15 * 60
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no background task to cleanup expired keys as of yet.
|
||||||
|
For now you can limit the number of keys fetched by having a simple whitelist.
|
|
@ -0,0 +1,20 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var keyfetch = require('./keyfetch.js');
|
||||||
|
|
||||||
|
keyfetch.init({});
|
||||||
|
keyfetch.oidcJwks("https://bigsquid.auth0.com").then(function (jwks) {
|
||||||
|
console.log(jwks);
|
||||||
|
return keyfetch.oidcJwk(jwks[0].thumbprint, "https://bigsquid.auth0.com").then(function (jwk) {
|
||||||
|
console.log(jwk);
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
var jwt = '...';
|
||||||
|
keyfetch.verify({ jwt: jwt }).catch(function (err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
*/
|
|
@ -0,0 +1,301 @@
|
||||||
|
'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 mincache = 1 * 60 * 60;
|
||||||
|
var maxcache = 3 * 24 * 60 * 60;
|
||||||
|
var staletime = 15 * 60;
|
||||||
|
var keyCache = {};
|
||||||
|
|
||||||
|
/*global Promise*/
|
||||||
|
function checkMinDefaultMax(opts, key, n, d, x) {
|
||||||
|
var i = opts[key];
|
||||||
|
if (!i && 0 !== i) { return d; }
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
keyfetch._oidc = function (iss) {
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
return requestAsync({
|
||||||
|
url: normalizeIss(iss) + '/.well-known/openid-configuration'
|
||||||
|
, json: true
|
||||||
|
}).then(function (resp) {
|
||||||
|
var oidcConf = resp.body;
|
||||||
|
if (!oidcConf.jwks_uri) {
|
||||||
|
throw new Error("Failed to retrieve openid configuration");
|
||||||
|
}
|
||||||
|
return oidcConf;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch._wellKnownJwks = function (iss) {
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
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) {
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
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 keyfetch._setCache(result.jwk.iss || jwkUrl, result);
|
||||||
|
})).then(function () {
|
||||||
|
// cacheable -> hit (keep original externally immutable)
|
||||||
|
return JSON.parse(JSON.stringify(results));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch.wellKnownJwks = function (iss) {
|
||||||
|
// TODO DRY up a bit
|
||||||
|
return keyfetch._wellKnownJwks(iss).then(function (results) {
|
||||||
|
return Promise.all(results.map(function (result) {
|
||||||
|
return keyfetch._setCache(result.jwk.iss || iss, result);
|
||||||
|
})).then(function () {
|
||||||
|
// result -> hit (keep original externally immutable)
|
||||||
|
return JSON.parse(JSON.stringify(results));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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 keyfetch._setCache(result.jwk.iss || iss, result);
|
||||||
|
})).then(function () {
|
||||||
|
// result -> hit (keep original externally immutable)
|
||||||
|
return JSON.parse(JSON.stringify(results));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch.oidcJwk = function (id, iss) {
|
||||||
|
// TODO [2] DRY this up a bit
|
||||||
|
return keyfetch._checkCache(id, iss).then(function (hit) {
|
||||||
|
if (hit) {
|
||||||
|
return Promise.resolve(hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyfetch.oidcJwks(iss).then(function (results) {
|
||||||
|
var result = results.some(function (result) {
|
||||||
|
// we already checked iss above
|
||||||
|
return result.jwk.kid === id || result.thumbprint === id;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch.wellKnownJwk = function (id, iss) {
|
||||||
|
// TODO [2] DRY this up a bit
|
||||||
|
return keyfetch._checkCache(id, iss).then(function (hit) {
|
||||||
|
if (hit) {
|
||||||
|
return Promise.resolve(hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyfetch.wellKnownJwks(iss).then(function (results) {
|
||||||
|
var result = results.some(function (result) {
|
||||||
|
// we already checked iss above
|
||||||
|
return result.jwk.kid === id || result.thumbprint === id;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch.jwk = function (id, jwksUrl) {
|
||||||
|
// TODO [2] DRY this up a bit
|
||||||
|
return keyfetch._checkCache(id, jwksUrl).then(function (hit) {
|
||||||
|
if (hit) {
|
||||||
|
return Promise.resolve(hit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyfetch.jwks(jwksUrl).then(function (results) {
|
||||||
|
var result = results.some(function (result) {
|
||||||
|
// we already checked iss above
|
||||||
|
return result.jwk.kid === id || result.thumbprint === id;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch._checkCache = function (id, iss) {
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
// We cache by thumbprint and (kid + '@' + iss),
|
||||||
|
// so it's safe to check without appending the issuer
|
||||||
|
var hit = keyCache[id];
|
||||||
|
if (!hit) {
|
||||||
|
hit = keyCache[id + '@' + normalizeIss(iss)];
|
||||||
|
}
|
||||||
|
if (!hit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = Math.round(Date.now() / 1000);
|
||||||
|
var left = hit.expiresAt - now;
|
||||||
|
// not guarding number checks since we know that we
|
||||||
|
// set 'now' and 'expiresAt' correctly elsewhere
|
||||||
|
if (left > staletime) {
|
||||||
|
return JSON.parse(JSON.stringify(hit));
|
||||||
|
}
|
||||||
|
if (left > 0) {
|
||||||
|
return JSON.parse(JSON.stringify(hit));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
keyfetch._setCache = function (iss, cacheable) {
|
||||||
|
// force into a number
|
||||||
|
var expiresAt = parseInt(cacheable.jwk.exp, 10) || 0;
|
||||||
|
var now = Date.now() / 1000;
|
||||||
|
var left = expiresAt - now;
|
||||||
|
|
||||||
|
// TODO maybe log out when any of these non-ideal cases happen?
|
||||||
|
if (!left) {
|
||||||
|
expiresAt = now + maxcache;
|
||||||
|
} else if (left < mincache) {
|
||||||
|
expiresAt = now + mincache;
|
||||||
|
} else if (left > maxcache) {
|
||||||
|
expiresAt = now + maxcache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheable = { jwk, thumprint, pem }
|
||||||
|
cacheable.createdAt = now;
|
||||||
|
cacheable.expiresAt = expiresAt;
|
||||||
|
keyCache[cacheable.thumbprint] = cacheable;
|
||||||
|
keyCache[cacheable.jwk.kid + '@' + normalizeIss(iss)] = cacheable;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeIss(iss) {
|
||||||
|
// We definitely don't want false negatives stemming
|
||||||
|
// from https://example.com vs https://example.com/
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
return iss.replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
keyfetch._decode = function (jwt) {
|
||||||
|
var parts = jwt.split('.');
|
||||||
|
return {
|
||||||
|
header: JSON.parse(Buffer.from(parts[0], 'base64'))
|
||||||
|
, payload: JSON.parse(Buffer.from(parts[1], 'base64'))
|
||||||
|
, signature: parts[2] //Buffer.from(parts[2], 'base64')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
keyfetch.verify = function (opts) {
|
||||||
|
var jwt = opts.jwt;
|
||||||
|
return Promise.resolve().then(function () {
|
||||||
|
var decoded;
|
||||||
|
var exp;
|
||||||
|
var nbf;
|
||||||
|
var valid;
|
||||||
|
try {
|
||||||
|
decoded = keyfetch._decode(jwt);
|
||||||
|
exp = decoded.payload.exp;
|
||||||
|
nbf = decoded.payload.nbf;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("could not parse opts.jwt: '" + jwt + "'");
|
||||||
|
}
|
||||||
|
if (exp) {
|
||||||
|
valid = (parseInt(exp, 10) - (Date.now()/1000) > 0);
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error("token's 'exp' has passed or could not parsed: '" + exp + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nbf) {
|
||||||
|
valid = (parseInt(nbf, 10) - (Date.now()/1000) <= 0);
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var kid = decoded.header.kid;
|
||||||
|
var iss;
|
||||||
|
var fetcher;
|
||||||
|
if (!opts.strategy || 'oidc' === opts.strategy) {
|
||||||
|
iss = decoded.payload.iss;
|
||||||
|
fetcher = keyfetch.oidcJwks;
|
||||||
|
} else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) {
|
||||||
|
iss = decoded.payload.iss;
|
||||||
|
fetcher = keyfetch.wellKnownJwks;
|
||||||
|
} else {
|
||||||
|
iss = opts.strategy;
|
||||||
|
fetcher = keyfetch.jwks;
|
||||||
|
}
|
||||||
|
function verify(jwk, payload) {
|
||||||
|
var alg = 'RSA-SHA' + decoded.header.alg.replace(/[^\d]+/i, '');
|
||||||
|
return require('crypto')
|
||||||
|
.createVerify(alg)
|
||||||
|
.update(jwt.split('.')[0] + '.' + payload)
|
||||||
|
.verify(jwk.pem, decoded.signature, 'base64');
|
||||||
|
}
|
||||||
|
return fetcher(iss).then(function (jwks) {
|
||||||
|
var payload = jwt.split('.')[1]; // as string, as it was signed
|
||||||
|
if (jwks.some(function (jwk) {
|
||||||
|
if (kid) {
|
||||||
|
if (kid !== jwk.kid && kid !== jwk.thumbprint) { return; }
|
||||||
|
if (verify(jwk, payload)) { return true; }
|
||||||
|
throw new Error('token signature verification was unsuccessful');
|
||||||
|
} else {
|
||||||
|
if (verify(jwk, payload)) { return true; }
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "keyfetch",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@coolaj86/urequest": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA=="
|
||||||
|
},
|
||||||
|
"eckles": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA=="
|
||||||
|
},
|
||||||
|
"rasha": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{ "author": {
|
||||||
|
"name": "AJ ONeal",
|
||||||
|
"email": "solderjs@gmail.com"
|
||||||
|
},
|
||||||
|
"bundleDependencies": false,
|
||||||
|
"dependencies": {
|
||||||
|
"@coolaj86/urequest": "^1.3.6",
|
||||||
|
"eckles": "^1.4.0",
|
||||||
|
"rasha": "^1.2.1"
|
||||||
|
},
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "Lightweight support for fetching JWKs.",
|
||||||
|
"files": [
|
||||||
|
"keyfetch-test.js"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"jwks",
|
||||||
|
"jwk",
|
||||||
|
"jwt",
|
||||||
|
"auth0",
|
||||||
|
"pem",
|
||||||
|
"RSA",
|
||||||
|
"EC",
|
||||||
|
"ECDSA",
|
||||||
|
"OIDC",
|
||||||
|
"well-known"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"main": "keyfetch.js",
|
||||||
|
"name": "keyfetch",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"version": "1.1.1"
|
||||||
|
}
|
Loading…
Reference in New Issue