An easy, lightweight, and secure module for fetching JWK Public Keys and verifying JWTs. Great for OIDC, Auth0, JWKs URLs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
AJ ONeal aba70bf0ff make Prettier 2 years ago
.gitignore v1.1.8: cleanup and support forced keys for verification 3 years ago
.prettierrc make Prettier 2 years ago
LICENSE v1.1.10: update package.json and LICENSE 3 years ago
README.md make Prettier 2 years ago
keyfetch-test.js make Prettier 2 years ago
keyfetch.js make Prettier 2 years ago
package-lock.json v1.1.9: bump deps 3 years ago
package.json v1.2.1: better trusted issuer normalization 3 years ago

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)
  • Other JWKs URLs

Crypto Support

  • JWT verification
  • RSA (all variants)
  • EC / ECDSA (NIST variants P-256, P-384)
  • esoteric variants (excluded to keep the code featherweight and secure)

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 which issuers are allowed (otherwise anyone can create a valid token with whatever any claims they want).

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/' ]
, 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)
  • 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

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.