308 lines
9.2 KiB
Markdown
308 lines
9.2 KiB
Markdown
# [keyfetch](https://git.rootprojects.org/root/keyfetch.js)
|
|
|
|
Lightweight support for fetching JWKs.
|
|
|
|
Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the `jsonwebtoken` package
|
|
(and node's native RSA and ECDSA crypto APIs).
|
|
|
|
## Features
|
|
|
|
Works great for
|
|
|
|
- [x] `jsonwebtoken` (Auth0)
|
|
- [x] OIDC (OpenID Connect)
|
|
- [x] .well-known/jwks.json (Auth0, Okta)
|
|
- [x] Other JWKs URLs
|
|
|
|
Crypto Support
|
|
|
|
- [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
|
|
|
|
```bash
|
|
npm install --save keyfetch
|
|
```
|
|
|
|
# Usage
|
|
|
|
Retrieve a key list of keys:
|
|
|
|
```js
|
|
var keyfetch = require("keyfetch");
|
|
|
|
keyfetch.oidcJwks("https://example.com/").then(function (results) {
|
|
results.forEach(function (result) {
|
|
console.log(result.jwk);
|
|
console.log(result.thumprint);
|
|
console.log(result.pem);
|
|
});
|
|
});
|
|
```
|
|
|
|
Quick JWT verification (for authentication):
|
|
|
|
```js
|
|
var keyfetch = require("keyfetch");
|
|
var jwt = "...";
|
|
|
|
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);
|
|
});
|
|
```
|
|
|
|
Verify a JWT with `jsonwebtoken`:
|
|
|
|
```js
|
|
var keyfetch = require("keyfetch");
|
|
var jwt = require("jsonwebtoken");
|
|
var auth = "..."; // some JWT
|
|
var token = jwt.decode(auth, { json: true, complete: true });
|
|
|
|
if (!isTrustedIssuer(token.payload.iss)) {
|
|
throw new Error("untrusted issuer");
|
|
}
|
|
|
|
keyfetch.oidcJwk(token.header.kid, token.payload.iss).then(function (result) {
|
|
console.log(result.jwk);
|
|
console.log(result.thumprint);
|
|
console.log(result.pem);
|
|
|
|
jwt.jwt.verify(jwt, { jwk: result.jwk });
|
|
});
|
|
```
|
|
|
|
_Note_: You might implement `isTrustedIssuer` one of these:
|
|
|
|
```js
|
|
function isTrustedIssuer(iss) {
|
|
return -1 !== ["https://partner.com/", "https://auth0.com/"].indexOf(iss);
|
|
}
|
|
```
|
|
|
|
```js
|
|
function isTrustedIssuer(iss) {
|
|
return (
|
|
/^https:/.test(iss) && /(\.|^)example\.com$/.test(iss) // must be a secure domain
|
|
); // can be example.com or any subdomain
|
|
}
|
|
```
|
|
|
|
# API
|
|
|
|
All API calls will return the RFC standard JWK SHA256 thumbprint as well as a PEM version of the key.
|
|
|
|
Note: When specifying `id`, it may be either `kid` (as in `token.header.kid`)
|
|
or `thumbprint` (as in `result.thumbprint`).
|
|
|
|
### JWKs URLs
|
|
|
|
Retrieves keys from a URL such as `https://example.com/jwks/` with the format `{ keys: [ { kid, kty, exp, ... } ] }`
|
|
and returns the array of keys (as well as thumbprint and jwk-to-pem).
|
|
|
|
```js
|
|
keyfetch.jwks(jwksUrl);
|
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
|
```
|
|
|
|
```js
|
|
keyfetch.jwk(id, jwksUrl);
|
|
// Promises { jwk, thumbprint, pem } or fails
|
|
```
|
|
|
|
### Auth0
|
|
|
|
If `https://example.com/` is used as `issuerUrl` it will resolve to
|
|
`https://example.com/.well-known/jwks.json` and return the keys.
|
|
|
|
```js
|
|
keyfetch.wellKnownJwks(issuerUrl);
|
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
|
```
|
|
|
|
```js
|
|
keyfetch.wellKnownJwk(id, issuerUrl);
|
|
// Promises { jwk, thumbprint, pem } or fails
|
|
```
|
|
|
|
### OIDC
|
|
|
|
If `https://example.com/` is used as `issuerUrl` then it will first resolve to
|
|
`https://example.com/.well-known/openid-configuration` and then follow `jwks_uri` to return the keys.
|
|
|
|
```js
|
|
keyfetch.oidcJwks(issuerUrl);
|
|
// Promises [ { jwk, thumbprint, pem } ] or fails
|
|
```
|
|
|
|
```js
|
|
keyfetch.oidcJwk(id, issuerUrl);
|
|
// Promises { jwk, thumbprint, pem } or fails
|
|
```
|
|
|
|
### Verify JWT
|
|
|
|
This can accept a _JWT string_ (compact JWS) or a _decoded JWT object_ (JWS).
|
|
|
|
This can be used purely for verifying pure authentication tokens, as well as authorization tokens.
|
|
|
|
```js
|
|
keyfetch.jwt.verify(jwt, { strategy: "oidc" }).then(function (verified) {
|
|
/*
|
|
{ protected: '...' // base64 header
|
|
, payload: '...' // base64 payload
|
|
, signature: '...' // base64 signature
|
|
, header: {...} // decoded header
|
|
, claims: {...} // decoded payload
|
|
}
|
|
*/
|
|
});
|
|
```
|
|
|
|
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,
|
|
|
|
// expire each key after 3 days (regardless of jwk.exp)
|
|
maxcache: 3 * 24 * 60 * 60,
|
|
|
|
// re-fetch a key up to 15 minutes before it expires (only if used)
|
|
staletime: 15 * 60
|
|
});
|
|
```
|
|
|
|
There is no background task to cleanup expired keys as of yet.
|
|
For now you can limit the number of keys fetched by having a simple whitelist.
|
|
|
|
# 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.
|
|
- The `message` property of an error 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).
|
|
|
|
For backwards compatibility with v1, the non-stringified `message` is the same as what it was in v1 (and the v2 message is `client_message`, which replaces `message` in v3). Don't rely on it. Rely on `code`.
|
|
|
|
| Hint | Code | Status | Message (truncated) |
|
|
| ------------------- | --------------- | ------ | ------------------------------------------------ |
|
|
| (developer error) | DEVELOPER_ERROR | 500 | test... |
|
|
| (bad gateway) | BAD_GATEWAY | 502 | The token could not be verified because our s... |
|
|
| (insecure issuer) | MALFORMED_JWT | 400 | 'test' is NOT secure. Set env 'KEYFETCH_ALLOW... |
|
|
| (parse error) | MALFORMED_JWT | 400 | could not parse jwt: 'test'... |
|
|
| (no issuer) | MALFORMED_JWT | 400 | 'iss' is not defined... |
|
|
| (malformed exp) | MALFORMED_JWT | 400 | token's 'exp' has passed or could not parsed:... |
|
|
| (expired) | INVALID_JWT | 401 | token's 'exp' has passed or could not parsed:... |
|
|
| (inactive) | INVALID_JWT | 401 | token's 'nbf' has not been reached or could n... |
|
|
| (bad signature) | INVALID_JWT | 401 | token signature verification was unsuccessful... |
|
|
| (jwk not found old) | INVALID_JWT | 401 | Retrieved a list of keys, but none of them ma... |
|
|
| (jwk not found) | INVALID_JWT | 401 | No JWK found by kid or thumbprint 'test'... |
|
|
| (no jwkws uri) | INVALID_JWT | 401 | Failed to retrieve openid configuration... |
|
|
| (unknown issuer) | INVALID_JWT | 401 | token was issued by an untrusted issuer: 'tes... |
|
|
| (failed claims) | INVALID_JWT | 401 | token did not match on one or more authorizat... |
|
|
|
|
# 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`)
|
|
- 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).
|