AJ ONeal b728731394 | ||
---|---|---|
lib | ||
.gitignore | ||
.jshintrc | ||
.prettierrc.json | ||
CHANGELOG.md | ||
LICENSE | ||
README.md | ||
keyfetch-test.js | ||
keyfetch.js | ||
package-lock.json | ||
package.json |
README.md
keyfetch
Lightweight support for fetching JWKs.
Fetches JSON native JWKs and exposes them as PEMs that can be consumed by the jsonwebtoken
package
(and node's native RSA and ECDSA crypto APIs).
Features
Works great for
jsonwebtoken
(Auth0)- OIDC (OpenID Connect)
- .well-known/jwks.json (Auth0, Okta)
- Other JWKs URLs
Crypto Support
- JWT verification
- RSA (all variants)
- EC / ECDSA (NIST variants P-256, P-384)
- 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
npm install --save keyfetch
Usage
Retrieve a key list of keys:
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):
var keyfetch = require("keyfetch");
var jwt = "...";
keyfetch.jwt.verify(jwt).then(function (decoded) {
console.log(decoded);
});
JWT verification (for authorization):
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
:
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:
function isTrustedIssuer(iss) {
return -1 !== ["https://partner.com/", "https://auth0.com/"].indexOf(iss);
}
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).
keyfetch.jwks(jwksUrl);
// Promises [ { jwk, thumbprint, pem } ] or fails
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.
keyfetch.wellKnownJwks(issuerUrl);
// Promises [ { jwk, thumbprint, pem } ] or fails
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.
keyfetch.oidcJwks(issuerUrl);
// Promises [ { jwk, thumbprint, pem } ] or fails
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.
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.
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 beoidc
(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 likeissuers
, but only oneclaims
is an object with arbitrary keys (i.e. everything except for the standardiat
,exp
,jti
, etc)exp
may be set tofalse
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
try {
console.log( keyfetch.jwt.decode(jwt) );
} catch(e) {
console.error(e);
}
{ 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.
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
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:
{
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 isdetail
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.