mirror of
https://github.com/therootcompany/keyfetch.js.git
synced 2025-03-13 03:50:54 +00:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
a84e833571 | |||
076246e4d0 | |||
4c85fc4009 | |||
d5647ea905 | |||
2ea44e3a46 | |||
5ef53ecb23 | |||
604b42c7ef | |||
c57a08f0cd | |||
a3539b0941 | |||
523a4f0d1a | |||
7d5889b4de | |||
79e6758f9f | |||
842807f92b | |||
a648de58d8 | |||
8ec2d98645 | |||
9141962456 | |||
aba70bf0ff | |||
a32b942861 |
22
.jshintrc
Normal file
22
.jshintrc
Normal 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
7
.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"useTabs": false
|
||||||
|
}
|
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal 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).
|
163
README.md
163
README.md
@ -1,4 +1,4 @@
|
|||||||
# keyfetch
|
# [keyfetch](https://git.rootprojects.org/root/keyfetch.js)
|
||||||
|
|
||||||
Lightweight support for fetching JWKs.
|
Lightweight support for fetching JWKs.
|
||||||
|
|
||||||
@ -9,17 +9,28 @@ Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `j
|
|||||||
|
|
||||||
Works great for
|
Works great for
|
||||||
|
|
||||||
* [x] `jsonwebtoken` (Auth0)
|
- [x] `jsonwebtoken` (Auth0)
|
||||||
* [x] OIDC (OpenID Connect)
|
- [x] OIDC (OpenID Connect)
|
||||||
* [x] .well-known/jwks.json (Auth0)
|
- [x] .well-known/jwks.json (Auth0, Okta)
|
||||||
* [x] Other JWKs URLs
|
- [x] Other JWKs URLs
|
||||||
|
|
||||||
Crypto Support
|
Crypto Support
|
||||||
|
|
||||||
* [x] JWT verification
|
- [x] JWT verification
|
||||||
* [x] RSA (all variants)
|
- [x] RSA (all variants)
|
||||||
* [x] EC / ECDSA (NIST variants P-256, P-384)
|
- [x] EC / ECDSA (NIST variants P-256, P-384)
|
||||||
* [ ] esoteric variants (excluded to keep the code featherweight and secure)
|
- [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
|
# Install
|
||||||
|
|
||||||
@ -32,7 +43,7 @@ npm install --save keyfetch
|
|||||||
Retrieve a key list of keys:
|
Retrieve a key list of keys:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var keyfetch = require('keyfetch');
|
var keyfetch = require("keyfetch");
|
||||||
|
|
||||||
keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
||||||
results.forEach(function (result) {
|
results.forEach(function (result) {
|
||||||
@ -46,8 +57,8 @@ keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
|||||||
Quick JWT verification (for authentication):
|
Quick JWT verification (for authentication):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var keyfetch = require('keyfetch');
|
var keyfetch = require("keyfetch");
|
||||||
var jwt = '...';
|
var jwt = "...";
|
||||||
|
|
||||||
keyfetch.jwt.verify(jwt).then(function (decoded) {
|
keyfetch.jwt.verify(jwt).then(function (decoded) {
|
||||||
console.log(decoded);
|
console.log(decoded);
|
||||||
@ -57,7 +68,7 @@ keyfetch.jwt.verify(jwt).then(function (decoded) {
|
|||||||
JWT verification (for authorization):
|
JWT verification (for authorization):
|
||||||
|
|
||||||
```js
|
```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) {
|
keyfetch.jwt.verify(jwt, options).then(function (decoded) {
|
||||||
console.log(decoded);
|
console.log(decoded);
|
||||||
});
|
});
|
||||||
@ -66,19 +77,16 @@ keyfetch.jwt.verify(jwt, options).then(function (decoded) {
|
|||||||
Verify a JWT with `jsonwebtoken`:
|
Verify a JWT with `jsonwebtoken`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var keyfetch = require('keyfetch');
|
var keyfetch = require("keyfetch");
|
||||||
var jwt = require('jsonwebtoken');
|
var jwt = require("jsonwebtoken");
|
||||||
var auth = "..."; // some JWT
|
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)) {
|
if (!isTrustedIssuer(token.payload.iss)) {
|
||||||
throw new Error("untrusted issuer");
|
throw new Error("untrusted issuer");
|
||||||
}
|
}
|
||||||
|
|
||||||
keyfetch.oidcJwk(
|
keyfetch.oidcJwk(token.header.kid, token.payload.iss).then(function (result) {
|
||||||
token.header.kid
|
|
||||||
, token.payload.iss
|
|
||||||
).then(function (result) {
|
|
||||||
console.log(result.jwk);
|
console.log(result.jwk);
|
||||||
console.log(result.thumprint);
|
console.log(result.thumprint);
|
||||||
console.log(result.pem);
|
console.log(result.pem);
|
||||||
@ -87,18 +95,19 @@ keyfetch.oidcJwk(
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
*Note*: You might implement `isTrustedIssuer` one of these:
|
_Note_: You might implement `isTrustedIssuer` one of these:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function isTrustedIssuer(iss) {
|
function isTrustedIssuer(iss) {
|
||||||
return -1 !== [ 'https://partner.com/', 'https://auth0.com/'].indexOf(iss);
|
return -1 !== ["https://partner.com/", "https://auth0.com/"].indexOf(iss);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function isTrustedIssuer(iss) {
|
function isTrustedIssuer(iss) {
|
||||||
return /^https:/.test(iss) && // must be a secure domain
|
return (
|
||||||
/(\.|^)example\.com$/.test(iss); // can be example.com or any subdomain
|
/^https:/.test(iss) && /(\.|^)example\.com$/.test(iss) // must be a secure domain
|
||||||
|
); // can be example.com or any subdomain
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -115,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).
|
and returns the array of keys (as well as thumbprint and jwk-to-pem).
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.jwks(jwksUrl)
|
keyfetch.jwks(jwksUrl);
|
||||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.jwk(id, jwksUrl)
|
keyfetch.jwk(id, jwksUrl);
|
||||||
// Promises { jwk, thumbprint, pem } or fails
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -130,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.
|
`https://example.com/.well-known/jwks.json` and return the keys.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.wellKnownJwks(issuerUrl)
|
keyfetch.wellKnownJwks(issuerUrl);
|
||||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.wellKnownJwk(id, issuerUrl)
|
keyfetch.wellKnownJwk(id, issuerUrl);
|
||||||
// Promises { jwk, thumbprint, pem } or fails
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -145,12 +154,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.
|
`https://example.com/.well-known/openid-configuration` and then follow `jwks_uri` to return the keys.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.oidcJwks(issuerUrl)
|
keyfetch.oidcJwks(issuerUrl);
|
||||||
// Promises [ { jwk, thumbprint, pem } ] or fails
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.oidcJwk(id, issuerUrl)
|
keyfetch.oidcJwk(id, issuerUrl);
|
||||||
// Promises { jwk, thumbprint, pem } or fails
|
// Promises { jwk, thumbprint, pem } or fails
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -161,7 +170,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.
|
This can be used purely for verifying pure authentication tokens, as well as authorization tokens.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.jwt.verify(jwt, { strategy: 'oidc' }).then(function (verified) {
|
keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
|
||||||
/*
|
/*
|
||||||
{ protected: '...' // base64 header
|
{ protected: '...' // base64 header
|
||||||
, payload: '...' // base64 payload
|
, payload: '...' // base64 payload
|
||||||
@ -173,26 +182,27 @@ keyfetch.jwt.verify(jwt, { strategy: 'oidc' }).then(function (verified) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
When used for authorization, it's important to specify which `issuers` are allowed
|
When used for authorization, it's important to specify a limited set of trusted `issuers`. \
|
||||||
(otherwise anyone can create a valid token with whatever any claims they want).
|
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.
|
If your authorization `claims` can be expressed as exact string matches, you can specify those too.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
keyfetch.jwt.verify(jwt, {
|
keyfetch.jwt.verify(jwt, {
|
||||||
strategy: 'oidc'
|
strategy: 'oidc',
|
||||||
, issuers: [ 'https://example.com/' ]
|
issuers: [ 'https://example.com/' ],
|
||||||
, claims: { role: 'admin', sub: 'abc', group: 'xyz' }
|
//iss: 'https://example.com/',
|
||||||
|
claims: { role: 'admin', sub: 'abc', group: 'xyz' }
|
||||||
}).then(function (verified) {
|
}).then(function (verified) {
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url.
|
- `strategy` may be `oidc` (default) , `auth0`, or a direct JWKs url.
|
||||||
* `issuers` must be a list of https urls (though http is allowed for things like Docker swarm)
|
- `issuers` must be a list of https urls (though http is allowed for things like Docker swarm), or '\*'
|
||||||
* `claims` is an object with arbitrary keys (i.e. everything except for the standard `iat`, `exp`, `jti`, etc)
|
- `iss` is like `issuers`, but only one
|
||||||
* `exp` may be set to `false` if you're validating on your own (i.e. allowing time drift leeway)
|
- `claims` is an object with arbitrary keys (i.e. everything except for the standard `iat`, `exp`, `jti`, etc)
|
||||||
* `jwks` can be used to specify a list of allowed public key rather than fetching them (i.e. for offline unit tests)
|
- `exp` may be set to `false` if you're validating on your own (i.e. allowing time drift leeway)
|
||||||
* `jwk` same as above, but a single key rather than a list
|
- `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
|
### Decode JWT
|
||||||
|
|
||||||
@ -217,16 +227,16 @@ It's easier just to show the code than to explain the example.
|
|||||||
```js
|
```js
|
||||||
keyfetch.jwt.decode = function (jwt) {
|
keyfetch.jwt.decode = function (jwt) {
|
||||||
// Unpack JWS from "compact" form
|
// Unpack JWS from "compact" form
|
||||||
var parts = jwt.split('.');
|
var parts = jwt.split(".");
|
||||||
var obj = {
|
var obj = {
|
||||||
protected: parts[0]
|
protected: parts[0],
|
||||||
, payload: parts[1]
|
payload: parts[1],
|
||||||
, signature: parts[2]
|
signature: parts[2]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Decode JWT properties from JWS as unordered objects
|
// Decode JWT properties from JWS as unordered objects
|
||||||
obj.header = JSON.parse(Buffer.from(obj.protected, 'base64'));
|
obj.header = JSON.parse(Buffer.from(obj.protected, "base64"));
|
||||||
obj.claims = JSON.parse(Buffer.from(obj.payload, 'base64'));
|
obj.claims = JSON.parse(Buffer.from(obj.payload, "base64"));
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
@ -237,15 +247,62 @@ keyfetch.jwt.decode = function (jwt) {
|
|||||||
```js
|
```js
|
||||||
keyfetch.init({
|
keyfetch.init({
|
||||||
// set all keys at least 1 hour (regardless of jwk.exp)
|
// 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)
|
// 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)
|
// 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.
|
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.
|
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).
|
||||||
|
155
keyfetch-test.js
155
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";
|
var testIss = "https://example.auth0.com";
|
||||||
|
|
||||||
keyfetch.init({});
|
keyfetch.init({});
|
||||||
keyfetch.oidcJwks(testIss).then(function (hits) {
|
keyfetch
|
||||||
|
.oidcJwks(testIss)
|
||||||
|
.then(function (hits) {
|
||||||
keyfetch._clear();
|
keyfetch._clear();
|
||||||
//console.log(hits);
|
//console.log(hits);
|
||||||
return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () {
|
return keyfetch.oidcJwk(hits[0].thumbprint, testIss).then(function () {
|
||||||
@ -12,65 +14,134 @@ keyfetch.oidcJwks(testIss).then(function (hits) {
|
|||||||
//console.log(jwk);
|
//console.log(jwk);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).then(function () {
|
})
|
||||||
|
.then(function () {
|
||||||
console.log("Fetching PASSES");
|
console.log("Fetching PASSES");
|
||||||
}).catch(function (err) {
|
})
|
||||||
|
.catch(function (err) {
|
||||||
console.error("NONE SHALL PASS!");
|
console.error("NONE SHALL PASS!");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*global Promise*/
|
/*global Promise*/
|
||||||
var keypairs = require('keypairs.js');
|
var keypairs = require("keypairs");
|
||||||
keypairs.generate().then(function (pair) {
|
keypairs.generate().then(function (pair) {
|
||||||
|
var iss = "https://example.com/";
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
keypairs.signJwt({
|
keypairs
|
||||||
jwk: pair.private, iss: 'https://example.com/', sub: 'mikey', exp: '1h'
|
.signJwt({
|
||||||
}).then(function (jwt) {
|
jwk: pair.private,
|
||||||
|
iss: iss,
|
||||||
|
sub: "mikey",
|
||||||
|
exp: "1h"
|
||||||
|
})
|
||||||
|
.then(function (jwt) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
keyfetch.jwt.verify(jwt, { jwk: pair.public }).then(function (verified) {
|
keyfetch.jwt.verify(jwt, { jwk: pair.public, iss: "*" }).then(function (verified) {
|
||||||
if (!(verified.claims && verified.claims.exp)) {
|
if (!(verified.claims && verified.claims.exp)) {
|
||||||
throw new Error("malformed decoded token");
|
throw new Error("malformed decoded token");
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
, keyfetch.jwt.verify(keyfetch.jwt.decode(jwt), { jwk: pair.public }).then(function (verified) {
|
keyfetch.jwt
|
||||||
|
.verify(keyfetch.jwt.decode(jwt), { jwk: pair.public, iss: iss })
|
||||||
|
.then(function (verified) {
|
||||||
if (!(verified.claims && verified.claims.exp)) {
|
if (!(verified.claims && verified.claims.exp)) {
|
||||||
throw new Error("malformed decoded token");
|
throw new Error("malformed decoded token");
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
keyfetch.jwt.verify(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"]
|
||||||
})
|
})
|
||||||
, keyfetch.jwt.verify(jwt, { jwks: [pair.public] })
|
.then(e("bad scheme"))
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com/'] })
|
.catch(throwIfNotExpected),
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com'] })
|
keyfetch.jwt
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['example.com'] })
|
.verify(jwt, {
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['example.com/'] })
|
jwk: pair.public,
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['*'] })
|
issuers: ["https://www.example.com"]
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['http://example.com'] })
|
})
|
||||||
.then(e("bad scheme")).catch(throwIfNotExpected)
|
.then(e("bad prefix"))
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://www.example.com'] })
|
.catch(throwIfNotExpected),
|
||||||
.then(e("bad prefix")).catch(throwIfNotExpected)
|
keyfetch.jwt
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://wexample.com'] })
|
.verify(jwt, {
|
||||||
.then(e("bad sld")).catch(throwIfNotExpected)
|
jwk: pair.public,
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.comm'] })
|
issuers: ["https://wexample.com"]
|
||||||
.then(e("bad tld")).catch(throwIfNotExpected)
|
})
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com/' } })
|
.then(e("bad sld"))
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, claims: { iss: 'https://example.com' } })
|
.catch(throwIfNotExpected),
|
||||||
.then(e("inexact claim")).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({
|
.then(function (jwt) {
|
||||||
jwk: pair.private, iss: false, sub: 'mikey', exp: '1h'
|
|
||||||
}).then(function (jwt) {
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
keyfetch.jwt.verify(jwt, { jwk: pair.public })
|
// test that the old behavior of defaulting to '*' still works
|
||||||
, keyfetch.jwt.verify(jwt)
|
keyfetch.jwt
|
||||||
.then(e("should have an issuer")).catch(throwIfNotExpected)
|
.verify(jwt, { jwk: pair.public })
|
||||||
, keyfetch.jwt.verify(jwt, { jwk: pair.public, issuers: ['https://example.com/'] })
|
.then(e("should have issued security warning about allow all by default"))
|
||||||
.then(e("fail when issuer specified and doesn't exist")).catch(throwIfNotExpected)
|
.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 () {
|
])
|
||||||
|
.then(function () {
|
||||||
console.log("JWT PASSES");
|
console.log("JWT PASSES");
|
||||||
}).catch(function (err) {
|
})
|
||||||
|
.catch(function (err) {
|
||||||
console.error("NONE SHALL PASS!");
|
console.error("NONE SHALL PASS!");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -87,5 +158,7 @@ function e(msg) {
|
|||||||
return new Error("ETEST: " + msg);
|
return new Error("ETEST: " + msg);
|
||||||
}
|
}
|
||||||
function throwIfNotExpected(err) {
|
function throwIfNotExpected(err) {
|
||||||
if ("ETEST" === err.message.slice(0, 5)) { throw err; }
|
if ("ETEST" === err.message.slice(0, 5)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
413
keyfetch.js
413
keyfetch.js
@ -1,118 +1,119 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
|
|
||||||
var keyfetch = module.exports;
|
var keyfetch = module.exports;
|
||||||
|
|
||||||
var promisify = require('util').promisify;
|
var request = require("@root/request").defaults({
|
||||||
var requestAsync = promisify(require('@coolaj86/urequest'));
|
userAgent: "keyfetch/v2.1.0"
|
||||||
var Rasha = require('rasha');
|
});
|
||||||
var Eckles = require('eckles');
|
var Rasha = require("rasha");
|
||||||
|
var Eckles = require("eckles");
|
||||||
var mincache = 1 * 60 * 60;
|
var mincache = 1 * 60 * 60;
|
||||||
var maxcache = 3 * 24 * 60 * 60;
|
var maxcache = 3 * 24 * 60 * 60;
|
||||||
var staletime = 15 * 60;
|
var staletime = 15 * 60;
|
||||||
var keyCache = {};
|
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) {
|
function checkMinDefaultMax(opts, key, n, d, x) {
|
||||||
var i = opts[key];
|
var i = opts[key];
|
||||||
if (!i && 0 !== i) { return d; }
|
if (!i && 0 !== i) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
if (i >= n && i >= x) {
|
if (i >= n && i >= x) {
|
||||||
return parseInt(i, 10);
|
return parseInt(i, 10);
|
||||||
} else {
|
} 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._errors = Errors;
|
||||||
|
|
||||||
keyfetch._clear = function () {
|
keyfetch._clear = function () {
|
||||||
keyCache = {};
|
keyCache = {};
|
||||||
};
|
};
|
||||||
keyfetch.init = function (opts) {
|
keyfetch.init = function (opts) {
|
||||||
mincache = checkMinDefaultMax(opts, 'mincache',
|
mincache = checkMinDefaultMax(opts, "mincache", 1 * 60, mincache, 31 * 24 * 60 * 60);
|
||||||
1 * 60,
|
maxcache = checkMinDefaultMax(opts, "maxcache", 1 * 60 * 60, maxcache, 31 * 24 * 60 * 60);
|
||||||
mincache,
|
staletime = checkMinDefaultMax(opts, "staletime", 1 * 60, staletime, 31 * 24 * 60 * 60);
|
||||||
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) {
|
keyfetch._oidc = async function (iss) {
|
||||||
return Promise.resolve().then(function () {
|
var url = normalizeIss(iss) + "/.well-known/openid-configuration";
|
||||||
return requestAsync({
|
var resp = await requestAsync({
|
||||||
url: normalizeIss(iss) + '/.well-known/openid-configuration'
|
url: url,
|
||||||
, json: true
|
json: true
|
||||||
}).then(function (resp) {
|
});
|
||||||
|
|
||||||
var oidcConf = resp.body;
|
var oidcConf = resp.body;
|
||||||
if (!oidcConf.jwks_uri) {
|
if (!oidcConf.jwks_uri) {
|
||||||
throw new Error("Failed to retrieve openid configuration");
|
throw Errors.NO_JWKS_URI(url);
|
||||||
}
|
}
|
||||||
return oidcConf;
|
return oidcConf;
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch._wellKnownJwks = function (iss) {
|
keyfetch._wellKnownJwks = async 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) {
|
keyfetch._jwks = async function (iss) {
|
||||||
return requestAsync({ url: iss, json: true }).then(function (resp) {
|
var resp = await requestAsync({ url: iss, json: true });
|
||||||
return Promise.all(resp.body.keys.map(function (jwk) {
|
|
||||||
|
return Promise.all(
|
||||||
|
resp.body.keys.map(async function (jwk) {
|
||||||
// EC keys have an x values, whereas RSA keys do not
|
// EC keys have an x values, whereas RSA keys do not
|
||||||
var Keypairs = jwk.x ? Eckles : Rasha;
|
var Keypairs = jwk.x ? Eckles : Rasha;
|
||||||
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumbprint) {
|
var thumbprint = await Keypairs.thumbprint({ jwk: jwk });
|
||||||
return Keypairs.export({ jwk: jwk }).then(function (pem) {
|
var pem = await Keypairs.export({ jwk: jwk });
|
||||||
var cacheable = {
|
var cacheable = {
|
||||||
jwk: jwk
|
jwk: jwk,
|
||||||
, thumbprint: thumbprint
|
thumbprint: thumbprint,
|
||||||
, pem: pem
|
pem: pem
|
||||||
};
|
};
|
||||||
return cacheable;
|
return cacheable;
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
}));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.jwks = function (jwkUrl) {
|
keyfetch.jwks = async function (jwkUrl) {
|
||||||
// TODO DRY up a bit
|
// TODO DRY up a bit
|
||||||
return keyfetch._jwks(jwkUrl).then(function (results) {
|
var results = await keyfetch._jwks(jwkUrl);
|
||||||
return Promise.all(results.map(function (result) {
|
await Promise.all(
|
||||||
|
results.map(async function (result) {
|
||||||
return keyfetch._setCache(result.jwk.iss || jwkUrl, result);
|
return keyfetch._setCache(result.jwk.iss || jwkUrl, result);
|
||||||
})).then(function () {
|
})
|
||||||
|
);
|
||||||
// cacheable -> hit (keep original externally immutable)
|
// cacheable -> hit (keep original externally immutable)
|
||||||
return JSON.parse(JSON.stringify(results));
|
return JSON.parse(JSON.stringify(results));
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.wellKnownJwks = function (iss) {
|
keyfetch.wellKnownJwks = async function (iss) {
|
||||||
// TODO DRY up a bit
|
// TODO DRY up a bit
|
||||||
return keyfetch._wellKnownJwks(iss).then(function (results) {
|
var results = await keyfetch._wellKnownJwks(iss);
|
||||||
return Promise.all(results.map(function (result) {
|
await Promise.all(
|
||||||
|
results.map(async function (result) {
|
||||||
return keyfetch._setCache(result.jwk.iss || iss, result);
|
return keyfetch._setCache(result.jwk.iss || iss, result);
|
||||||
})).then(function () {
|
})
|
||||||
|
);
|
||||||
// result -> hit (keep original externally immutable)
|
// result -> hit (keep original externally immutable)
|
||||||
return JSON.parse(JSON.stringify(results));
|
return JSON.parse(JSON.stringify(results));
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.oidcJwks = function (iss) {
|
keyfetch.oidcJwks = async function (iss) {
|
||||||
return keyfetch._oidc(iss).then(function (oidcConf) {
|
var oidcConf = await keyfetch._oidc(iss);
|
||||||
// TODO DRY up a bit
|
// TODO DRY up a bit
|
||||||
return keyfetch._jwks(oidcConf.jwks_uri).then(function (results) {
|
var results = await keyfetch._jwks(oidcConf.jwks_uri);
|
||||||
return Promise.all(results.map(function (result) {
|
await Promise.all(
|
||||||
|
results.map(async function (result) {
|
||||||
return keyfetch._setCache(result.jwk.iss || iss, result);
|
return keyfetch._setCache(result.jwk.iss || iss, result);
|
||||||
})).then(function () {
|
})
|
||||||
|
);
|
||||||
// result -> hit (keep original externally immutable)
|
// result -> hit (keep original externally immutable)
|
||||||
return JSON.parse(JSON.stringify(results));
|
return JSON.parse(JSON.stringify(results));
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
function checkId(id) {
|
function checkId(id) {
|
||||||
return function (results) {
|
return function (results) {
|
||||||
@ -122,36 +123,38 @@ function checkId(id) {
|
|||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error("No JWK found by kid or thumbprint '" + id + "'");
|
throw Errors.JWK_NOT_FOUND(id);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
keyfetch.oidcJwk = function (id, iss) {
|
keyfetch.oidcJwk = async function (id, iss) {
|
||||||
return keyfetch._checkCache(id, iss).then(function (hit) {
|
var hit = await keyfetch._checkCache(id, iss);
|
||||||
if (hit) { return hit; }
|
if (hit) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
return keyfetch.oidcJwks(iss).then(checkId(id));
|
return keyfetch.oidcJwks(iss).then(checkId(id));
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.wellKnownJwk = function (id, iss) {
|
keyfetch.wellKnownJwk = async function (id, iss) {
|
||||||
return keyfetch._checkCache(id, iss).then(function (hit) {
|
var hit = await keyfetch._checkCache(id, iss);
|
||||||
if (hit) { return hit; }
|
if (hit) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
return keyfetch.wellKnownJwks(iss).then(checkId(id));
|
return keyfetch.wellKnownJwks(iss).then(checkId(id));
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.jwk = function (id, jwksUrl) {
|
keyfetch.jwk = async function (id, jwksUrl) {
|
||||||
return keyfetch._checkCache(id, jwksUrl).then(function (hit) {
|
var hit = await keyfetch._checkCache(id, jwksUrl);
|
||||||
if (hit) { return hit; }
|
if (hit) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
return keyfetch.jwks(jwksUrl).then(checkId(id));
|
return keyfetch.jwks(jwksUrl).then(checkId(id));
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch._checkCache = function (id, iss) {
|
keyfetch._checkCache = async function (id, iss) {
|
||||||
return Promise.resolve().then(function () {
|
|
||||||
// We cache by thumbprint and (kid + '@' + iss),
|
// We cache by thumbprint and (kid + '@' + iss),
|
||||||
// so it's safe to check without appending the issuer
|
// so it's safe to check without appending the issuer
|
||||||
var hit = keyCache[id];
|
var hit = keyCache[id];
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
hit = keyCache[id + '@' + normalizeIss(iss)];
|
hit = keyCache[id + "@" + normalizeIss(iss)];
|
||||||
}
|
}
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
return null;
|
return null;
|
||||||
@ -168,7 +171,6 @@ keyfetch._checkCache = function (id, iss) {
|
|||||||
return JSON.parse(JSON.stringify(hit));
|
return JSON.parse(JSON.stringify(hit));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch._setCache = function (iss, cacheable) {
|
keyfetch._setCache = function (iss, cacheable) {
|
||||||
// force into a number
|
// force into a number
|
||||||
@ -189,91 +191,129 @@ keyfetch._setCache = function (iss, cacheable) {
|
|||||||
cacheable.createdAt = now;
|
cacheable.createdAt = now;
|
||||||
cacheable.expiresAt = expiresAt;
|
cacheable.expiresAt = expiresAt;
|
||||||
keyCache[cacheable.thumbprint] = cacheable;
|
keyCache[cacheable.thumbprint] = cacheable;
|
||||||
keyCache[cacheable.jwk.kid + '@' + normalizeIss(iss)] = cacheable;
|
keyCache[cacheable.jwk.kid + "@" + normalizeIss(iss)] = cacheable;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeIss(iss) {
|
function normalizeIss(iss) {
|
||||||
|
if (!iss) {
|
||||||
|
throw Errors.NO_ISSUER();
|
||||||
|
}
|
||||||
|
|
||||||
// We definitely don't want false negatives stemming
|
// We definitely don't want false negatives stemming
|
||||||
// from https://example.com vs https://example.com/
|
// from https://example.com vs https://example.com/
|
||||||
// We also don't want to allow insecure issuers
|
// We also don't want to allow insecure issuers
|
||||||
if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) {
|
if (/^http:/.test(iss) && !process.env.KEYFETCH_ALLOW_INSECURE_HTTP) {
|
||||||
// note, we wrap some things in promises just so we can throw here
|
// 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.jwt = {};
|
keyfetch.jwt = {};
|
||||||
keyfetch.jwt.decode = function (jwt) {
|
keyfetch.jwt.decode = function (jwt) {
|
||||||
var parts = jwt.split('.');
|
try {
|
||||||
|
var parts = jwt.split(".");
|
||||||
// JWS
|
// JWS
|
||||||
var obj = {
|
var obj = { protected: parts[0], payload: parts[1], signature: parts[2] };
|
||||||
protected: parts[0]
|
|
||||||
, payload: parts[1]
|
|
||||||
, signature: parts[2]
|
|
||||||
};
|
|
||||||
// JWT
|
// JWT
|
||||||
obj.header = JSON.parse(Buffer.from(obj.protected, 'base64'));
|
obj.header = JSON.parse(Buffer.from(obj.protected, "base64"));
|
||||||
obj.claims = JSON.parse(Buffer.from(obj.payload, 'base64'));
|
obj.claims = JSON.parse(Buffer.from(obj.payload, "base64"));
|
||||||
return obj;
|
return obj;
|
||||||
|
} catch (e) {
|
||||||
|
var err = Errors.PARSE_ERROR(jwt);
|
||||||
|
err.details = e.message;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
keyfetch.jwt.verify = function (jwt, opts) {
|
keyfetch.jwt.verify = async function (jwt, opts) {
|
||||||
if (!opts) { opts = {}; }
|
if (!opts) {
|
||||||
return Promise.resolve().then(function () {
|
opts = {};
|
||||||
var decoded;
|
}
|
||||||
|
|
||||||
|
var jws;
|
||||||
var exp;
|
var exp;
|
||||||
var nbf;
|
var nbf;
|
||||||
var active;
|
var active;
|
||||||
var issuers = opts.issuers || ['*'];
|
var now;
|
||||||
|
var then;
|
||||||
|
var issuers = opts.issuers || [];
|
||||||
|
if (opts.iss) {
|
||||||
|
issuers.push(opts.iss);
|
||||||
|
}
|
||||||
|
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 || {};
|
var claims = opts.claims || {};
|
||||||
if (!jwt || 'string' === typeof jwt) {
|
if (!jwt || "string" === typeof jwt) {
|
||||||
try { decoded = keyfetch.jwt.decode(jwt); }
|
jws = keyfetch.jwt.decode(jwt);
|
||||||
catch (e) { throw new Error("could not parse jwt: '" + jwt + "'"); }
|
|
||||||
} else {
|
} else {
|
||||||
decoded = jwt;
|
jws = jwt;
|
||||||
}
|
}
|
||||||
exp = decoded.claims.exp;
|
|
||||||
nbf = decoded.claims.nbf;
|
|
||||||
|
|
||||||
if (!issuers.some(isTrustedIssuer(decoded.claims.iss))) {
|
if (!jws.claims.iss || !issuers.some(isTrustedIssuer(jws.claims.iss))) {
|
||||||
throw new Error("token was issued by an untrusted issuer: '" + decoded.claims.iss + "'");
|
if (!(opts.jwk || opts.jwks)) {
|
||||||
|
throw Errors.UNKNOWN_ISSUER(jws.claims.iss || "");
|
||||||
}
|
}
|
||||||
// TODO verify claims also?
|
}
|
||||||
if (!Object.keys(claims).every(function (key) {
|
// Note claims.iss validates more strictly than opts.issuers (requires exact match)
|
||||||
if (claims[key] === decoded.claims[key]) {
|
var failedClaims = Object.keys(claims)
|
||||||
|
.filter(function (key) {
|
||||||
|
if (claims[key] !== jws.claims[key]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})) {
|
})
|
||||||
throw new Error("token did not match on one or more authorization claims: '" + Object.keys(claims) + "'");
|
.map(function (key) {
|
||||||
|
return "jwt.claims." + key + " = " + JSON.stringify(jws.claims[key]);
|
||||||
|
});
|
||||||
|
if (failedClaims.length) {
|
||||||
|
throw Errors.FAILED_CLAIMS(failedClaims, Object.keys(claims));
|
||||||
}
|
}
|
||||||
|
|
||||||
active = ((opts.exp || 0) + parseInt(exp, 10) - (Date.now()/1000) > 0);
|
exp = jws.claims.exp;
|
||||||
if (!active) {
|
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
|
// expiration was on the token or, if not, such a token is not allowed
|
||||||
if (exp || false !== opts.exp) {
|
|
||||||
throw new Error("token's 'exp' has passed or could not parsed: '" + exp + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nbf) {
|
|
||||||
active = (parseInt(nbf, 10) - (Date.now()/1000) <= 0);
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
throw new Error("token's 'nbf' has not been reached or could not parsed: '" + nbf + "'");
|
throw Errors.EXPIRED(exp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nbf = jws.claims.nbf;
|
||||||
|
if (nbf) {
|
||||||
|
active = parseInt(nbf, 10) - Date.now() / 1000 <= 0;
|
||||||
|
if (!active) {
|
||||||
|
throw Errors.INACTIVE(nbf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (opts.jwks || opts.jwk) {
|
if (opts.jwks || opts.jwk) {
|
||||||
return overrideLookup(opts.jwks || [opts.jwk]);
|
return overrideLookup(opts.jwks || [opts.jwk]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var kid = decoded.header.kid;
|
var kid = jws.header.kid;
|
||||||
var iss;
|
var iss;
|
||||||
var fetcher;
|
var fetcher;
|
||||||
var fetchOne;
|
var fetchOne;
|
||||||
if (!opts.strategy || 'oidc' === opts.strategy) {
|
if (!opts.strategy || "oidc" === opts.strategy) {
|
||||||
iss = decoded.claims.iss;
|
iss = jws.claims.iss;
|
||||||
fetcher = keyfetch.oidcJwks;
|
fetcher = keyfetch.oidcJwks;
|
||||||
fetchOne = keyfetch.oidcJwk;
|
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;
|
iss = jws.claims.iss;
|
||||||
fetcher = keyfetch.wellKnownJwks;
|
fetcher = keyfetch.wellKnownJwks;
|
||||||
fetchOne = keyfetch.wellKnownJwk;
|
fetchOne = keyfetch.wellKnownJwk;
|
||||||
} else {
|
} else {
|
||||||
@ -282,57 +322,59 @@ keyfetch.jwt.verify = function (jwt, opts) {
|
|||||||
fetchOne = keyfetch.jwk;
|
fetchOne = keyfetch.jwk;
|
||||||
}
|
}
|
||||||
|
|
||||||
var p;
|
|
||||||
if (kid) {
|
if (kid) {
|
||||||
p = fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
|
return fetchOne(kid, iss).then(verifyOne); //.catch(fetchAny);
|
||||||
} else {
|
|
||||||
p = fetcher(iss).then(verifyAny);
|
|
||||||
}
|
}
|
||||||
return p;
|
return fetcher(iss).then(verifyAny);
|
||||||
|
|
||||||
function verifyOne(hit) {
|
function verifyOne(hit) {
|
||||||
if (true === keyfetch.jws.verify(decoded, hit)) {
|
if (true === keyfetch.jws.verify(jws, hit)) {
|
||||||
return decoded;
|
return jws;
|
||||||
}
|
}
|
||||||
throw new Error('token signature verification was unsuccessful');
|
throw Errors.BAD_SIGNATURE(jws.protected + "." + jws.payload + "." + jws.signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyAny(hits) {
|
function verifyAny(hits) {
|
||||||
if (hits.some(function (hit) {
|
if (
|
||||||
|
hits.some(function (hit) {
|
||||||
if (kid) {
|
if (kid) {
|
||||||
if (kid !== hit.jwk.kid && kid !== hit.thumbprint) { return; }
|
if (kid !== hit.jwk.kid && kid !== hit.thumbprint) {
|
||||||
if (true === keyfetch.jws.verify(decoded, hit)) { return true; }
|
return;
|
||||||
throw new Error('token signature verification was unsuccessful');
|
|
||||||
} else {
|
|
||||||
if (true === keyfetch.jws.verify(decoded, hit)) { return true; }
|
|
||||||
}
|
}
|
||||||
})) {
|
if (true === keyfetch.jws.verify(jws, hit)) {
|
||||||
return decoded;
|
return true;
|
||||||
}
|
}
|
||||||
throw new Error("Retrieved a list of keys, but none of them matched the 'kid' (key id) of the token.");
|
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) {
|
function overrideLookup(jwks) {
|
||||||
return Promise.all(jwks.map(function (jwk) {
|
return Promise.all(
|
||||||
|
jwks.map(async function (jwk) {
|
||||||
var Keypairs = jwk.x ? Eckles : Rasha;
|
var Keypairs = jwk.x ? Eckles : Rasha;
|
||||||
return Keypairs.export({ jwk: jwk }).then(function (pem) {
|
var pem = await Keypairs.export({ jwk: jwk });
|
||||||
return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
|
var thumb = await Keypairs.thumbprint({ jwk: jwk });
|
||||||
return { jwk: jwk, pem: pem, thumbprint: thumb };
|
return { jwk: jwk, pem: pem, thumbprint: thumb };
|
||||||
});
|
})
|
||||||
});
|
).then(verifyAny);
|
||||||
})).then(verifyAny);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
};
|
||||||
keyfetch.jws = {};
|
keyfetch.jws = {};
|
||||||
keyfetch.jws.verify = function (jws, pub) {
|
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 = ecdsaAsn1SigToJwtSig(jws.header, jws.signature);
|
var sig = ecdsaJoseSigToAsn1Sig(jws.header, jws.signature);
|
||||||
return require('crypto')
|
return require("crypto")
|
||||||
.createVerify(alg)
|
.createVerify(alg)
|
||||||
.update(jws.protected + '.' + jws.payload)
|
.update(jws.protected + "." + jws.payload)
|
||||||
.verify(pub.pem, sig, 'base64')
|
.verify(pub.pem, sig, "base64");
|
||||||
;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// old, gotta make sure nothing else uses this
|
// old, gotta make sure nothing else uses this
|
||||||
@ -340,54 +382,65 @@ keyfetch._decode = function (jwt) {
|
|||||||
var obj = keyfetch.jwt.decode(jwt);
|
var obj = keyfetch.jwt.decode(jwt);
|
||||||
return { header: obj.header, payload: obj.claims, signature: obj.signature };
|
return { header: obj.header, payload: obj.claims, signature: obj.signature };
|
||||||
};
|
};
|
||||||
keyfetch.verify = function (opts) {
|
keyfetch.verify = async function (opts) {
|
||||||
var jwt = opts.jwt;
|
var jwt = opts.jwt;
|
||||||
return keyfetch.jwt.verify(jwt, opts).then(function (obj) {
|
var obj = await keyfetch.jwt.verify(jwt, opts);
|
||||||
return { header: obj.header, payload: obj.claims, signature: obj.signature };
|
return { header: obj.header, payload: obj.claims, signature: obj.signature };
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function ecdsaAsn1SigToJwtSig(header, b64sig) {
|
function ecdsaJoseSigToAsn1Sig(header, b64sig) {
|
||||||
// ECDSA JWT signatures differ from "normal" ECDSA signatures
|
// ECDSA JWT signatures differ from "normal" ECDSA signatures
|
||||||
// https://tools.ietf.org/html/rfc7518#section-3.4
|
// 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 hlen = bufsig.byteLength / 2; // should be even
|
||||||
var r = bufsig.slice(0, hlen);
|
var r = bufsig.slice(0, hlen);
|
||||||
var s = bufsig.slice(hlen);
|
var s = bufsig.slice(hlen);
|
||||||
// unpad positive ints less than 32 bytes wide
|
// unpad positive ints less than 32 bytes wide
|
||||||
while (!r[0]) { r = r.slice(1); }
|
while (!r[0]) {
|
||||||
while (!s[0]) { s = s.slice(1); }
|
r = r.slice(1);
|
||||||
|
}
|
||||||
|
while (!s[0]) {
|
||||||
|
s = s.slice(1);
|
||||||
|
}
|
||||||
// pad (or re-pad) ambiguously non-negative BigInts to 33 bytes wide
|
// 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 & r[0]) {
|
||||||
if (0x80 & s[0]) { s = Buffer.concat([Buffer.from([0]), s]); }
|
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 len = 2 + r.byteLength + 2 + s.byteLength;
|
||||||
var head = [0x30];
|
var head = [0x30];
|
||||||
// hard code 0x80 + 1 because it won't be longer than
|
// hard code 0x80 + 1 because it won't be longer than
|
||||||
// two SHA512 plus two pad bytes (130 bytes <= 256)
|
// two SHA512 plus two pad bytes (130 bytes <= 256)
|
||||||
if (len >= 0x80) { head.push(0x81); }
|
if (len >= 0x80) {
|
||||||
|
head.push(0x81);
|
||||||
|
}
|
||||||
head.push(len);
|
head.push(len);
|
||||||
|
|
||||||
var buf = Buffer.concat([
|
var buf = Buffer.concat([
|
||||||
Buffer.from(head)
|
Buffer.from(head),
|
||||||
, Buffer.from([0x02, r.byteLength]), r
|
Buffer.from([0x02, r.byteLength]),
|
||||||
, Buffer.from([0x02, s.byteLength]), s
|
r,
|
||||||
|
Buffer.from([0x02, s.byteLength]),
|
||||||
|
s
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return buf.toString('base64')
|
return buf.toString("base64").replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
||||||
.replace(/-/g, '+')
|
|
||||||
.replace(/_/g, '/')
|
|
||||||
.replace(/=/g, '')
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTrustedIssuer(issuer) {
|
function isTrustedIssuer(issuer) {
|
||||||
return function (trusted) {
|
return function (trusted) {
|
||||||
if ('*' === trusted) { return true; }
|
if ("*" === trusted) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// TODO account for '*.example.com'
|
// TODO account for '*.example.com'
|
||||||
trusted = (/^http(s?):\/\//.test(trusted) ? trusted : ('https://' + trusted));
|
trusted = /^http(s?):\/\//.test(trusted) ? trusted : "https://" + trusted;
|
||||||
return issuer.replace(/\/$/, '') === trusted.replace(/\/$/, '') && trusted;
|
return issuer.replace(/\/$/, "") === trusted.replace(/\/$/, "") && trusted;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
270
lib/errors.js
Normal file
270
lib/errors.js
Normal 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));
|
||||||
|
}
|
71
package-lock.json
generated
71
package-lock.json
generated
@ -1,19 +1,78 @@
|
|||||||
{
|
{
|
||||||
"name": "keyfetch",
|
"name": "keyfetch",
|
||||||
"version": "1.1.8",
|
"version": "3.0.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "keyfetch",
|
||||||
|
"version": "3.0.2",
|
||||||
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coolaj86/urequest": {
|
"@root/request": "^1.8.0",
|
||||||
"version": "1.3.7",
|
"eckles": "^1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz",
|
"rasha": "^1.2.4"
|
||||||
"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA=="
|
},
|
||||||
|
"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": {
|
"eckles": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz",
|
||||||
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA=="
|
||||||
},
|
},
|
||||||
|
"keypairs": {
|
||||||
|
"version": "1.2.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz",
|
||||||
|
"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"eckles": "^1.4.1",
|
||||||
|
"rasha": "^1.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rasha": {
|
"rasha": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.4.tgz",
|
||||||
|
19
package.json
19
package.json
@ -1,22 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "keyfetch",
|
"name": "keyfetch",
|
||||||
"version": "1.2.1",
|
"version": "3.0.2",
|
||||||
"description": "Lightweight support for fetching JWKs.",
|
"description": "Lightweight support for fetching JWKs.",
|
||||||
"homepage": "https://git.coolaj86.com/coolaj86/keyfetch.js",
|
"homepage": "https://git.rootprojects.org/root/keyfetch.js",
|
||||||
"main": "keyfetch.js",
|
"main": "keyfetch.js",
|
||||||
"files": [],
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coolaj86/urequest": "^1.3.7",
|
"@root/request": "^1.8.0",
|
||||||
"eckles": "^1.4.1",
|
"eckles": "^1.4.1",
|
||||||
"rasha": "^1.2.4"
|
"rasha": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {},
|
"devDependencies": {
|
||||||
|
"keypairs": "^1.2.14"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node keyfetch-test.js"
|
"test": "node keyfetch-test.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.coolaj86.com/coolaj86/keyfetch.js.git"
|
"url": "https://git.rootprojects.org/root/keyfetch.js.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"jwks",
|
"jwks",
|
||||||
@ -30,7 +34,6 @@
|
|||||||
"OIDC",
|
"OIDC",
|
||||||
"well-known"
|
"well-known"
|
||||||
],
|
],
|
||||||
"author": "AJ ONeal <solderjs@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "MPL-2.0"
|
"license": "MPL-2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user