Compare commits

...

29 Commits
v1.1.2 ... main

Author SHA1 Message Date
a84e833571 3.0.2 2021-10-21 14:19:26 -06:00
076246e4d0 docs: sync error messages to docs 2021-10-21 14:19:23 -06:00
4c85fc4009 3.0.1 2021-10-21 13:37:30 -06:00
d5647ea905 docs: update errors list 2021-10-21 13:37:21 -06:00
2ea44e3a46 bugfix: properly stringify message and pass details 2021-10-21 13:37:12 -06:00
5ef53ecb23 3.0.0 2021-10-21 13:28:30 -06:00
604b42c7ef chore!: drop really, really old node support 2021-10-21 13:28:20 -06:00
c57a08f0cd 2.1.0 2021-10-21 13:22:08 -06:00
a3539b0941 feature: add additional (standardized) error messages 2021-10-21 13:21:41 -06:00
523a4f0d1a feature: add distinct error codes 2021-10-20 18:12:06 -06:00
7d5889b4de chore: add linter config 2021-10-20 18:08:27 -06:00
79e6758f9f 2.0.0 2021-06-15 17:22:46 -06:00
842807f92b security warning -> issuer error, async/await 2021-06-15 17:22:38 -06:00
a648de58d8 1.2.2 2021-06-15 17:05:14 -06:00
8ec2d98645 security: obey opts.iss, issue warning by default 2021-06-15 17:03:30 -06:00
9141962456 rename .prettierrc.json 2021-06-15 16:15:43 -06:00
aba70bf0ff make Prettier 2020-04-08 16:01:06 -04:00
a32b942861 typo fix: ecdsaAsn1SigToJwtSig => ecdsaJoseSigToAsn1Sig 2019-05-06 03:06:49 -06:00
e274e5368a v1.2.1: better trusted issuer normalization 2019-03-15 13:59:55 -06:00
e6de23532b add a few more tests 2019-03-15 13:53:49 -06:00
5060c505b6 v1.2.0: add code to check issuers and claims 2019-03-15 13:45:27 -06:00
9b77939455 v1.1.10: update package.json and LICENSE 2019-03-13 17:32:35 -06:00
029bc13fe8 v1.1.9: bump deps 2019-03-12 09:43:24 -06:00
5fef6a7430 v1.1.8: cleanup and support forced keys for verification 2019-03-09 02:50:14 -07:00
448b977963 v1.1.7: bugfix ecdsa signature padding 2019-03-08 19:15:24 -07:00
2c36227afd v1.1.6: bugfix verify and ecdsa keys 2019-03-08 18:00:41 -07:00
9e5ffd1fc9 v1.1.5: used cached keys on verify when possible 2019-02-26 23:11:26 -07:00
0f0d0807a0 v1.1.4: fix fetching bug 2019-02-25 16:17:26 -07:00
cfe814ebb0 v1.1.3: remove bogus log 2019-02-25 16:04:43 -07:00
11 changed files with 1189 additions and 322 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

22
.jshintrc Normal file
View File

@ -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
}

7
.prettierrc.json Normal file
View File

@ -0,0 +1,7 @@
{
"bracketSpacing": true,
"printWidth": 120,
"tabWidth": 4,
"trailingComma": "none",
"useTabs": false
}

11
CHANGELOG.md Normal file
View File

@ -0,0 +1,11 @@
# v3.0.0
**Breaking Change**: Standardize error `message`s (now they're more client-friendly).
# v2.1.0
Feature: Add `code`, `status`, and `details` to errors.
# v2.0.0
**Breaking Change**: require `issuers` array (rather than `["*"]` by default).

41
LICENSE Normal file
View File

@ -0,0 +1,41 @@
Copyright 2019 AJ ONeal
This is open source software; you can redistribute it and/or modify it under the
terms of either:
a) the "MIT License"
b) the "Apache-2.0 License"
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

213
README.md
View File

@ -1,4 +1,4 @@
# keyfetch
# [keyfetch](https://git.rootprojects.org/root/keyfetch.js)
Lightweight support for fetching JWKs.
@ -9,17 +9,28 @@ 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, Okta)
- [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)
- [x] Sane error codes
- [ ] esoteric variants (excluded to keep the code featherweight and secure)
# Table of Contents
- Install
- Usage
- API
- Auth0 / Okta
- OIDC
- Errors
- Change Log
# Install
@ -32,7 +43,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) {
@ -43,13 +54,22 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) {
});
```
Quick JWT verification:
Quick JWT verification (for authentication):
```js
var keyfetch = require('keyfetch');
var jwt = '...';
var keyfetch = require("keyfetch");
var jwt = "...";
keyfetch.verify({ jwt: jwt }).then(function (decoded) {
keyfetch.jwt.verify(jwt).then(function (decoded) {
console.log(decoded);
});
```
JWT verification (for authorization):
```js
var options = { issuers: ["https://example.com/"], claims: { role: "admin" } };
keyfetch.jwt.verify(jwt, options).then(function (decoded) {
console.log(decoded);
});
```
@ -57,39 +77,37 @@ keyfetch.verify({ jwt: jwt }).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);
jwt.verify(jwt, pem);
jwt.jwt.verify(jwt, { jwk: result.jwk });
});
```
*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
}
```
@ -106,12 +124,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
```
@ -121,12 +139,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
```
@ -136,38 +154,155 @@ 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
```
### Verify JWT
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.verify({ jwt: jwk, strategy: 'oidc' })
// Promises a decoded JWT { headers, payload, signature } or fails
keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
/*
{ protected: '...' // base64 header
, payload: '...' // base64 payload
, signature: '...' // base64 signature
, header: {...} // decoded header
, claims: {...} // decoded payload
}
*/
});
```
* `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url.
When used for authorization, it's important to specify a limited set of trusted `issuers`. \
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.
```js
keyfetch.jwt.verify(jwt, {
strategy: 'oidc',
issuers: [ 'https://example.com/' ],
//iss: 'https://example.com/',
claims: { role: 'admin', sub: 'abc', group: 'xyz' }
}).then(function (verified) {
```
- `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), 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)
- `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
```jwt
try {
console.log( keyfetch.jwt.decode(jwt) );
} catch(e) {
console.error(e);
}
```
```js
{ protected: '...' // base64 header
, payload: '...' // base64 payload
, signature: '...' // base64 signature
, header: {...} // decoded header
, claims: {...} // decoded payload
```
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 obj = {
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"));
return obj;
};
```
### Cache Settings
```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.
For now you can limit the number of keys fetched by having a simple whitelist.
# Errors
`JSON.stringify()`d errors look like this:
```js
{
code: "INVALID_JWT",
status: 401,
details: [ "jwt.claims.exp = 1634804500", "DEBUG: helpful message" ]
message: "token's 'exp' has passed or could not parsed: 1634804500"
}
```
SemVer Compatibility:
- `code` & `status` will remain the same.
- `message` is **NOT** included in the semver compatibility guarantee (we intend to make them more client-friendly), neither is `detail` at this time (but it will be once we decide on what it should be).
- `details` may be added to, but not subtracted from
| Hint | Code | Status | Message (truncated) |
| ----------------- | ------------- | ------ | ------------------------------------------------------ |
| bad gateway | BAD_GATEWAY | 502 | The auth token could not be verified because our se... |
| insecure issuer | MALFORMED_JWT | 400 | The auth token could not be verified because our se... |
| parse error | MALFORMED_JWT | 400 | The auth token could not be verified because it is ... |
| no issuer | MALFORMED_JWT | 400 | The auth token could not be verified because it doe... |
| malformed exp | MALFORMED_JWT | 400 | The auth token could not be verified because it's e... |
| expired | INVALID_JWT | 401 | The auth token is expired. To try again, go to the ... |
| inactive | INVALID_JWT | 401 | The auth token isn't valid yet. It's activation dat... |
| bad signature | INVALID_JWT | 401 | The auth token did not pass verification because it... |
| jwk not found old | INVALID_JWT | 401 | The auth token did not pass verification because ou... |
| jwk not found | INVALID_JWT | 401 | The auth token did not pass verification because ou... |
| no jwkws uri | INVALID_JWT | 401 | The auth token did not pass verification because it... |
| unknown issuer | INVALID_JWT | 401 | The auth token did not pass verification because it... |
| failed claims | INVALID_JWT | 401 | The auth token did not pass verification because it... |
# Change Log
Minor Breaking changes (with a major version bump):
- v3.0.0
- reworked error messages (also available in v2.1.0 as `client_message`)
- started using `let` and template strings (drops _really_ old node compat)
- v2.0.0
- changes from the default `issuers = ["*"]` to requiring that an issuer (or public jwk for verification) is specified
See other changes in [CHANGELOG.md](./CHANGELOG.md).

View File

@ -1,20 +1,164 @@
'use strict';
"use strict";
var keyfetch = require('./keyfetch.js');
var keyfetch = require("./keyfetch.js");
var testIss = "https://example.auth0.com";
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);
keyfetch
.oidcJwks(testIss)
.then(function (hits) {
keyfetch._clear();
//console.log(hits);
return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () {
return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function (/*jwk*/) {
//console.log(jwk);
});
}).catch(function (err) {
});
})
.then(function () {
console.log("Fetching PASSES");
})
.catch(function (err) {
console.error("NONE SHALL PASS!");
console.error(err);
});
process.exit(1);
});
/*global Promise*/
var keypairs = require("keypairs");
keypairs.generate().then(function (pair) {
var iss = "https://example.com/";
return Promise.all([
keypairs
.signJwt({
jwk: pair.private,
iss: iss,
sub: "mikey",
exp: "1h"
})
.then(function (jwt) {
return Promise.all([
keyfetch.jwt.verify(jwt, { jwk: pair.public, iss: "*" }).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, iss: iss })
.then(function (verified) {
if (!(verified.claims && verified.claims.exp)) {
throw new Error("malformed decoded token");
}
}),
keyfetch.jwt.verify(jwt, { jwks: [pair.public], issuers: [iss] }),
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)
]);
}),
keypairs
.signJwt({
jwk: pair.private,
iss: false,
sub: "mikey",
exp: "1h"
})
.then(function (jwt) {
return Promise.all([
// test that the old behavior of defaulting to '*' still works
keyfetch.jwt
.verify(jwt, { jwk: pair.public })
.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).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 () {
console.log("JWT PASSES");
})
.catch(function (err) {
console.error("NONE SHALL PASS!");
console.error(err);
process.exit(1);
});
});
/*
var jwt = '...';
keyfetch.verify({ jwt: jwt }).catch(function (err) {
console.log(err);
});
*/
function e(msg) {
return new Error("ETEST: " + msg);
}
function throwIfNotExpected(err) {
if ("ETEST" === err.message.slice(0, 5)) {
throw err;
}
}

View File

@ -1,155 +1,160 @@
'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 request = require("@root/request").defaults({
userAgent: "keyfetch/v2.1.0"
});
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*/
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({ response: resp });
}
return resp;
}
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 {
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);
}
}
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._errors = Errors;
keyfetch._clear = function () {
keyCache = {};
};
keyfetch._oidc = function (iss) {
return Promise.resolve().then(function () {
return requestAsync({
url: normalizeIss(iss) + '/.well-known/openid-configuration'
, json: true
}).then(function (resp) {
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 = async function (iss) {
var url = normalizeIss(iss) + "/.well-known/openid-configuration";
var resp = await requestAsync({
url: url,
json: true
});
var oidcConf = resp.body;
if (!oidcConf.jwks_uri) {
throw new Error("Failed to retrieve openid configuration");
throw Errors.NO_JWKS_URI(url);
}
return oidcConf;
});
});
};
keyfetch._wellKnownJwks = function (iss) {
return Promise.resolve().then(function () {
return keyfetch._jwks(normalizeIss(iss) + '/.well-known/jwks.json');
});
keyfetch._wellKnownJwks = async function (iss) {
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) {
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
var Keypairs = jwk.x ? Eckles : Rasha;
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) {
return Keypairs.export({ jwk: jwk }).then(function (pem) {
var thumbprint = await Keypairs.thumbprint({ jwk: jwk });
var pem = await Keypairs.export({ jwk: jwk });
var cacheable = {
jwk: jwk
, thumbprint: thumbprint
, pem: pem
jwk: jwk,
thumbprint: thumbprint,
pem: pem
};
return cacheable;
});
});
}));
});
})
);
};
keyfetch.jwks = function (jwkUrl) {
keyfetch.jwks = async function (jwkUrl) {
// TODO DRY up a bit
return keyfetch._jwks(jwkUrl).then(function (results) {
return Promise.all(results.map(function (result) {
var results = await keyfetch._jwks(jwkUrl);
await Promise.all(
results.map(async 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) {
keyfetch.wellKnownJwks = async function (iss) {
// TODO DRY up a bit
return keyfetch._wellKnownJwks(iss).then(function (results) {
return Promise.all(results.map(function (result) {
var results = await keyfetch._wellKnownJwks(iss);
await Promise.all(
results.map(async 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) {
keyfetch.oidcJwks = async function (iss) {
var oidcConf = await keyfetch._oidc(iss);
// TODO DRY up a bit
return keyfetch._jwks(oidcConf.jwks_uri).then(function (results) {
return Promise.all(results.map(function (result) {
var results = await keyfetch._jwks(oidcConf.jwks_uri);
await Promise.all(
results.map(async function (result) {
return keyfetch._setCache(result.jwk.iss || iss, result);
})).then(function () {
})
);
// result -> hit (keep original externally immutable)
return JSON.parse(JSON.stringify(results));
});
});
});
};
function checkId(id) {
return function (results) {
var result = results.some(function (result) {
var result = results.filter(function (result) {
// we already checked iss above
console.log(result);
return result.jwk.kid === id || result.thumbprint === id;
})[0];
if (!result) {
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
throw Errors.JWK_NOT_FOUND(id);
}
return result;
};
}
keyfetch.oidcJwk = function (id, iss) {
return keyfetch._checkCache(id, iss).then(function (hit) {
if (hit) { return hit; }
keyfetch.oidcJwk = async function (id, iss) {
var hit = await keyfetch._checkCache(id, iss);
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; }
keyfetch.wellKnownJwk = async function (id, iss) {
var hit = await keyfetch._checkCache(id, iss);
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; }
keyfetch.jwk = async function (id, jwksUrl) {
var hit = await keyfetch._checkCache(id, jwksUrl);
if (hit) {
return hit;
}
return keyfetch.jwks(jwksUrl).then(checkId(id));
});
};
keyfetch._checkCache = function (id, iss) {
return Promise.resolve().then(function () {
keyfetch._checkCache = async function (id, iss) {
// 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)];
hit = keyCache[id + "@" + normalizeIss(iss)];
}
if (!hit) {
return null;
@ -166,7 +171,6 @@ keyfetch._checkCache = function (id, iss) {
return JSON.parse(JSON.stringify(hit));
}
return null;
});
};
keyfetch._setCache = function (iss, cacheable) {
// force into a number
@ -187,87 +191,256 @@ 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) {
if (!iss) {
throw Errors.NO_ISSUER();
}
// 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.");
throw Errors.INSECURE_ISSUER(iss);
}
return iss.replace(/\/$/, '');
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.jwt = {};
keyfetch.jwt.decode = function (jwt) {
try {
var parts = jwt.split(".");
// JWS
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.PARSE_ERROR(jwt);
err.details = e.message;
throw err;
}
};
keyfetch.verify = function (opts) {
var jwt = opts.jwt;
return Promise.resolve().then(function () {
var decoded;
keyfetch.jwt.verify = async function (jwt, opts) {
if (!opts) {
opts = {};
}
var jws;
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 + "'");
var active;
var now;
var then;
var issuers = opts.issuers || [];
if (opts.iss) {
issuers.push(opts.iss);
}
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 (opts.claims && opts.claims.iss) {
issuers.push(opts.claims.iss);
}
if (!issuers.length) {
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) {
jws = keyfetch.jwt.decode(jwt);
} else {
jws = jwt;
}
if (!jws.claims.iss || !issuers.some(isTrustedIssuer(jws.claims.iss))) {
if (!(opts.jwk || opts.jwks)) {
throw Errors.UNKNOWN_ISSUER(jws.claims.iss || "");
}
}
// Note claims.iss validates more strictly than opts.issuers (requires exact match)
var failedClaims = Object.keys(claims)
.filter(function (key) {
if (claims[key] !== jws.claims[key]) {
return true;
}
})
.map(function (key) {
return "jwt.claims." + key + " = " + JSON.stringify(jws.claims[key]);
});
if (failedClaims.length) {
throw Errors.FAILED_CLAIMS(failedClaims, Object.keys(claims));
}
exp = jws.claims.exp;
if (exp && false !== opts.exp) {
now = Date.now();
// TODO document that opts.exp can be used as leeway? Or introduce opts.leeway?
// fair, but not necessary
exp = parseInt(exp, 10);
if (isNaN(exp)) {
throw Errors.MALFORMED_EXP(JSON.stringify(jws.claims.exp));
}
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 (!active) {
throw Errors.EXPIRED(exp);
}
}
nbf = jws.claims.nbf;
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 + "'");
active = parseInt(nbf, 10) - Date.now() / 1000 <= 0;
if (!active) {
throw Errors.INACTIVE(nbf);
}
}
var kid = decoded.header.kid;
if (opts.jwks || opts.jwk) {
return overrideLookup(opts.jwks || [opts.jwk]);
}
var kid = jws.header.kid;
var iss;
var fetcher;
if (!opts.strategy || 'oidc' === opts.strategy) {
iss = decoded.payload.iss;
var fetchOne;
if (!opts.strategy || "oidc" === opts.strategy) {
iss = jws.claims.iss;
fetcher = keyfetch.oidcJwks;
} else if ('auth0' === opts.strategy || 'well-known' === opts.strategy) {
iss = decoded.payload.iss;
fetchOne = keyfetch.oidcJwk;
} else if ("auth0" === opts.strategy || "well-known" === opts.strategy) {
iss = jws.claims.iss;
fetcher = keyfetch.wellKnownJwks;
fetchOne = keyfetch.wellKnownJwk;
} else {
iss = opts.strategy;
fetcher = keyfetch.jwks;
fetchOne = keyfetch.jwk;
}
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 fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
}
})) {
return decoded;
return fetcher(iss).then(verifyAny);
function verifyOne(hit) {
if (true === keyfetch.jws.verify(jws, hit)) {
return jws;
}
throw Errors.BAD_SIGNATURE(jws.protected + "." + jws.payload + "." + jws.signature);
}
function verifyAny(hits) {
if (
hits.some(function (hit) {
if (kid) {
if (kid !== hit.jwk.kid && kid !== hit.thumbprint) {
return;
}
if (true === keyfetch.jws.verify(jws, hit)) {
return true;
}
throw Errors.BAD_SIGNATURE();
}
if (true === keyfetch.jws.verify(jws, hit)) {
return true;
}
})
) {
return jws;
}
throw Errors.JWK_NOT_FOUND_OLD(kid);
}
function overrideLookup(jwks) {
return Promise.all(
jwks.map(async function (jwk) {
var Keypairs = jwk.x ? Eckles : Rasha;
var pem = await Keypairs.export({ jwk: jwk });
var thumb = await Keypairs.thumbprint({ jwk: jwk });
return { jwk: jwk, pem: pem, thumbprint: thumb };
})
).then(verifyAny);
}
throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.");
});
});
};
keyfetch.jws = {};
keyfetch.jws.verify = function (jws, pub) {
var alg = "SHA" + jws.header.alg.replace(/[^\d]+/i, "");
var sig = ecdsaJoseSigToAsn1Sig(jws.header, jws.signature);
return require("crypto")
.createVerify(alg)
.update(jws.protected + "." + jws.payload)
.verify(pub.pem, sig, "base64");
};
// old, gotta make sure nothing else uses this
keyfetch._decode = function (jwt) {
var obj = keyfetch.jwt.decode(jwt);
return { header: obj.header, payload: obj.claims, signature: obj.signature };
};
keyfetch.verify = async function (opts) {
var jwt = opts.jwt;
var obj = await keyfetch.jwt.verify(jwt, opts);
return { header: obj.header, payload: obj.claims, signature: obj.signature };
};
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;
}
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);
}
// 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]);
}
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);
}
head.push(len);
var buf = Buffer.concat([
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, "");
}
function isTrustedIssuer(issuer) {
return function (trusted) {
if ("*" === trusted) {
return true;
}
// TODO account for '*.example.com'
trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted;
return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted;
};
}

270
lib/errors.js Normal file
View File

@ -0,0 +1,270 @@
"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(old, msg, code, status, details) {
/** @type AuthError */
//@ts-ignore
let err = new Error(msg);
err.message = msg;
err._old_message = old;
err.code = code;
err.status = status;
if (details) {
err.details = details;
}
err.source = "keyfetch";
err.toJSON = toJSON;
err.toString = toString;
return err;
}
function toJSON() {
/*jshint validthis:true*/
return {
message: this.message,
status: this.status,
code: this.code,
details: this.details
};
}
function toString() {
/*jshint validthis:true*/
return this.stack + "\n" + JSON.stringify(this);
}
// 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_JWT - the token could not be verified - not parsable, missing claims, etc
var E_MALFORMED = "MALFORMED_JWT";
// INVALID_JWT - 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 (old, msg, details) {
return create(old, msg || old, E_DEVELOPER, 500, details);
},
BAD_GATEWAY: function (err) {
var msg =
"The auth token could not be verified because our server encountered a network error (or a bad gateway) when connecting to its issuing server.";
var details = [];
if (err.message) {
details.push("error.message = " + err.message);
}
if (err.response && err.response.statusCode) {
details.push("response.statusCode = " + err.response.statusCode);
}
return create(msg, msg, E_BAD_GATEWAY, 502, details);
},
//
// MALFORMED_TOKEN (dev / client)
//
/**
* @param {string} iss
* @returns {AuthError}
*/
INSECURE_ISSUER: function (iss) {
var old =
"'" + iss + "' is NOT secure. Set env 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow for testing. (iss)";
var details = [
"jwt.claims.iss = " + JSON.stringify(iss),
"DEBUG: Set ENV 'KEYFETCH_ALLOW_INSECURE_HTTP=true' to allow insecure issuers (for testing)."
];
var msg =
'The auth token could not be verified because our server could connect to its issuing server ("iss") securely.';
return create(old, msg, E_MALFORMED, 400, details);
},
/**
* @param {string} jwt
* @returns {AuthError}
*/
PARSE_ERROR: function (jwt) {
var old = "could not parse jwt: '" + jwt + "'";
var msg = "The auth token could not be verified because it is malformed.";
var details = ["jwt = " + JSON.stringify(jwt)];
return create(old, msg, E_MALFORMED, 400, details);
},
/**
* @param {string} iss
* @returns {AuthError}
*/
NO_ISSUER: function (iss) {
var old = "'iss' is not defined";
var msg = 'The auth token could not be verified because it doesn\'t specify an issuer ("iss").';
var details = ["jwt.claims.iss = " + JSON.stringify(iss)];
return create(old, msg, E_MALFORMED, 400, details);
},
/**
* @param {string} iss
* @returns {AuthError}
*/
MALFORMED_EXP: function (exp) {
var old = "token's 'exp' has passed or could not parsed: '" + exp + "'";
var msg = 'The auth token could not be verified because it\'s expiration date ("exp") could not be read';
var details = ["jwt.claims.exp = " + JSON.stringify(exp)];
return create(old, msg, E_MALFORMED, 400, details);
},
//
// INVALID_TOKEN (dev / client)
//
/**
* @param {number} exp
* @returns {AuthError}
*/
EXPIRED: function (exp) {
var old = "token's 'exp' has passed or could not parsed: '" + exp + "'";
// var msg = "The auth token did not pass verification because it is expired.not properly signed.";
var msg = "The auth token is expired. To try again, go to the main page and sign in.";
var details = ["jwt.claims.exp = " + JSON.stringify(exp)];
return create(old, msg, E_INVALID, 401, details);
},
/**
* @param {number} nbf
* @returns {AuthError}
*/
INACTIVE: function (nbf) {
var old = "token's 'nbf' has not been reached or could not parsed: '" + nbf + "'";
var msg = "The auth token isn't valid yet. It's activation date (\"nbf\") is in the future.";
var details = ["jwt.claims.nbf = " + JSON.stringify(nbf)];
return create(old, msg, E_INVALID, 401, details);
},
/** @returns {AuthError} */
BAD_SIGNATURE: function (jwt) {
var old = "token signature verification was unsuccessful";
var msg = "The auth token did not pass verification because it is not properly signed.";
var details = ["jwt = " + JSON.stringify(jwt)];
return create(old, msg, E_INVALID, 401, details);
},
/**
* @param {string} kid
* @returns {AuthError}
*/
JWK_NOT_FOUND_OLD: function (kid) {
var old = "Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.";
var msg =
'The auth token did not pass verification because our server couldn\'t find a mutually trusted verification key ("jwk").';
var details = ["jws.header.kid = " + JSON.stringify(kid)];
return create(old, msg, E_INVALID, 401, details);
},
/**
* @param {string} id
* @returns {AuthError}
*/
JWK_NOT_FOUND: function (id) {
// TODO Distinguish between when it's a kid vs thumbprint.
var old = "No JWK found by kid or thumbprint '" + id + "'";
var msg =
'The auth token did not pass verification because our server couldn\'t find a mutually trusted verification key ("jwk").';
var details = ["jws.header.kid = " + JSON.stringify(id)];
return create(old, msg, E_INVALID, 401, details);
},
/** @returns {AuthError} */
NO_JWKWS_URI: function (url) {
var old = "Failed to retrieve openid configuration";
var msg =
'The auth token did not pass verification because its issuing server did not list any verification keys ("jwks").';
var details = ["OpenID Provider Configuration: " + JSON.stringify(url)];
return create(old, msg, E_INVALID, 401, details);
},
/**
* @param {string} iss
* @returns {AuthError}
*/
UNKNOWN_ISSUER: function (iss) {
var old = "token was issued by an untrusted issuer: '" + iss + "'";
var msg = "The auth token did not pass verification because it wasn't issued by a server that we trust.";
var details = ["jwt.claims.iss = " + JSON.stringify(iss)];
return create(old, msg, E_INVALID, 401, details);
},
/**
* @param {Array<string>} details
* @returns {AuthError}
*/
FAILED_CLAIMS: function (details, claimNames) {
var old = "token did not match on one or more authorization claims: '" + claimNames + "'";
var msg =
'The auth token did not pass verification because it failed some of the verification criteria ("claims").';
return create(old, msg, E_INVALID, 401, details);
}
};
var Errors = module.exports;
// for README
if (require.main === module) {
let maxWidth = 54;
let header = ["Hint", "Code", "Status", "Message (truncated)"];
let widths = header.map(function (v) {
return Math.min(maxWidth, String(v).length);
});
let rows = [];
Object.keys(module.exports).forEach(function (k) {
//@ts-ignore
var E = module.exports[k];
var e = E("test");
var code = e.code;
var msg = e.message;
var hint = k.toLowerCase().replace(/_/g, " ");
widths[0] = Math.max(widths[0], String(hint).length);
widths[1] = Math.max(widths[1], String(code).length);
widths[2] = Math.max(widths[2], String(e.status).length);
widths[3] = Math.min(maxWidth, Math.max(widths[3], String(msg).length));
rows.push([hint, code, e.status, msg]);
});
rows.forEach(function (cols, i) {
let cells = cols.map(function (col, i) {
if (col.length > maxWidth) {
col = col.slice(0, maxWidth - 3);
col += "...";
}
return String(col).padEnd(widths[i], " ");
});
let out = `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[3].slice(0, widths[3])} |`;
//out = out.replace(/\| /g, " ").replace(/\|/g, "");
console.info(out);
if (i === 0) {
cells = cols.map(function (col, i) {
return "-".padEnd(widths[i], "-");
});
console.info(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[3]} |`);
}
});
console.log();
console.log(Errors.MALFORMED_EXP());
console.log();
console.log(JSON.stringify(Errors.MALFORMED_EXP(), null, 2));
}

83
package-lock.json generated
View File

@ -1,23 +1,82 @@
{
"name": "keyfetch",
"version": "1.1.0",
"lockfileVersion": 1,
"version": "3.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "keyfetch",
"version": "3.0.2",
"license": "MPL-2.0",
"dependencies": {
"@coolaj86/urequest": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.6.tgz",
"integrity": "sha512-9rBXLFSb5D19opGeXdD/WuiFJsA4Pk2r8VUGEAeUZUxB1a2zB47K85BKAx3Gy9i4nZwg22ejlJA+q9DVrpQlbA=="
"@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.8.0",
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.8.0.tgz",
"integrity": "sha512-HufCvoTwqR30OyKSjwg28W5QCUpypSJZpOYcJbC9PME5kI6cOYsccYs/6bXfsuEoarz8+YwBDrsuM1UdBMxMLw=="
},
"eckles": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.0.tgz",
"integrity": "sha512-Bm5dpwhsBuoCHvKCY3gAvP8XFyXH7im8uAu3szykpVNbFBdC+lOuV8vLC8fvTYRZBfFqB+k/P6ud/ZPVO2V2tA=="
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.1.tgz",
"integrity": "sha512-cs4Hu/rVF3/Qucq+V7lxSz449VfHNMVXJaeajAHno9H5FC1PWlmS4NM6IAX5jPKFF0IC2rOdHdf7iNxQuIWZag=="
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
"integrity": "sha512-GsIwKv+hYSumJyK9wkTDaERLwvWaGYh1WuI7JMTBISfYt13TkKFU/HFzlY4n72p8VfXZRUYm0AqaYhkZVxOC3Q=="
}
}
}

View File

@ -1,18 +1,27 @@
{ "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,
{
"name": "keyfetch",
"version": "3.0.2",
"description": "Lightweight support for fetching JWKs.",
"homepage": "https://git.rootprojects.org/root/keyfetch.js",
"main": "keyfetch.js",
"files": [
"keyfetch-test.js"
"lib"
],
"dependencies": {
"@root/request": "^1.8.0",
"eckles": "^1.4.1",
"rasha": "^1.2.4"
},
"devDependencies": {
"keypairs": "^1.2.14"
},
"scripts": {
"test": "node keyfetch-test.js"
},
"repository": {
"type": "git",
"url": "https://git.rootprojects.org/root/keyfetch.js.git"
},
"keywords": [
"jwks",
"jwk",
@ -25,11 +34,6 @@
"OIDC",
"well-known"
],
"license": "MPL-2.0",
"main": "keyfetch.js",
"name": "keyfetch",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"version": "1.1.2"
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0"
}