node-jwks-rsa

repository·master·Indexed 21 days ago

https://github.com/auth0/node-jwks-rsa

A library to retrieve RSA public signing keys from a JWKS (JSON Web Key Set) endpoint, commonly used for verifying JSON Web Tokens (JWTs). It includes built-in support for caching and rate limiting, and provides integration helpers for popular Node.js frameworks including express-jwt, hapi-auth-jwt2, koa-jwt, and passport-jwt.

Tokens
7.6K
Snippets
28
Records
33
Agent score
73%

What's inside jwks-rsa

  1. How jwks-rsa.koaJwtSecret works with koa-jwt

    master

    The integration between koa-jwt and jwks-rsa follows this lifecycle during a request:

    1. Token Decoding: koa-jwt decodes the incoming JWT and passes the request and the decoded token to jwksRsa.koaJwtSecret.
    2. Key Retrieval: jwks-rsa downloads the signing keys from the configured jwksUri and searches for a key where the kid matches the kid in the JWT header.
      • If no match is found: An error is thrown.
      • If a match is found: The specific signing key is passed back to koa-jwt.
    3. Validation: koa-jwt uses the provided key to validate the token's signature, expiration (exp), audience (aud), and issuer (iss).

    Key Features:

    • Caching: When cache: true is set, subsequent requests will use the cached keys instead of hitting the JWKS endpoint again.
    • Rate Limiting: When rateLimit: true is set, the library limits the frequency of requests to the JWKS endpoint to protect your authorization server.
  2. Enable caching for signing keys

    master

    By default, signing key verification results are cached to prevent excessive HTTP requests. If a kid is found, it is stored in an LRU cache. Subsequent requests for the same kid will serve the key from the cache.

    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      cache: true, // Default Value
      cacheMaxEntries: 5, // Default value
      cacheMaxAge: 600000, // Defaults to 10m
      jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
    });
    
    const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
    const key = await client.getSigningKey(kid);
    const signingKey = key.getPublicKey();
  3. How expressJwtSecret works with JWTs

    master

    The integration follows this lifecycle:

    1. Token Decoding: express-jwt decodes the incoming JWT and extracts the header (containing the kid) and payload.
    2. Key Retrieval: jwks-rsa uses the kid to look up the corresponding signing key from the configured jwksUri.
    3. Matching:
      • If a matching key is found, it is passed to express-jwt to validate the signature.
      • If no match is found, an error is thrown (which can be handled via handleSigningKeyError).
    4. Validation: express-jwt completes the validation process, checking the signature, expiration (exp), audience (aud), and issuer (iss).

    If cache is enabled, subsequent requests with the same kid will use the cached key instead of hitting the JWKS endpoint. If rateLimit is enabled, multiple requests with invalid kids will trigger rate limiting to protect your JWKS endpoint.

  4. How passportJwtSecret works with passport-jwt

    master

    The integration follows this lifecycle:

    1. Token Decoding: passport-jwt decodes the incoming JWT and passes the request, header, and payload to the secretOrKeyProvider (the function returned by jwksRsa.passportJwtSecret).
    2. Key Retrieval: jwks-rsa fetches the signing keys from the configured jwksUri. It searches the key set for a key where the kid matches the kid in the JWT header.
      • If a match is found, the signing key is passed back to passport-jwt.
      • If no match is found, an error is thrown (which can be handled via handleSigningKeyError).
    3. Validation: passport-jwt uses the provided key to validate the token's signature, expiration (exp), audience (aud), and issuer (iss).

    Key Features:

    • Caching: When cache: true is set, subsequent requests with the same keys do not trigger new calls to the JWKS endpoint.
    • Rate Limiting: When rateLimit: true is set, the library limits the number of requests made to the JWKS endpoint (controlled by jwksRequestsPerMinute) to prevent abuse from invalid kid values.
  5. Handle JWKS endpoint downtime with graceful degradation

    master

    To prevent service failure when the JWKS endpoint is unreachable after cacheMaxAge has expired, use cacheMaxAgeFallback. This setting allows the library to continue serving the last known good signing key for an additional duration.

    Note: This is an availability vs. security tradeoff. If keys are rotated during a compromise while the endpoint is down, stale keys will still be trusted until the fallback window expires.

    Use the onStaleCacheFallback callback to monitor when stale keys are being served (e.g., for alerting).

    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      cache: true,
      cacheMaxAge: 600000,          // 10 minutes — normal freshness TTL
      cacheMaxAgeFallback: 3600000, // 1 hour — serve stale key if JWKS endpoint is unreachable
      jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json',
      onStaleCacheFallback: (err, kid, staleKey) => {
        console.warn(`JWKS endpoint unavailable, serving stale key for kid '${kid}': ${err.message}`);
      }
    });
    
    const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
    const key = await client.getSigningKey(kid);
    const signingKey = key.getPublicKey();
  6. Implement rate limiting for JWKS requests

    master

    To protect your JWKS endpoint from attackers sending many random kids, enable rate limiting. This limits the number of calls made to the JWKS URI per minute.

    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      rateLimit: true,
      jwksRequestsPerMinute: 10, // Default value
      jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
    });
    
    const kid = 'RkI5MjI5OUY5ODc1N0Q4QzM0OUYzNkVGMTJDOUEzQkFCOTU3NjE2Rg';
    const key = await client.getSigningKey(kid);
    const signingKey = key.getPublicKey();
  7. Integrate jwks-rsa with express-jwt

    master

    You can use jwksRsa.expressJwtSecret to create a secret provider for express-jwt. This provider dynamically retrieves the correct signing key from a JWKS endpoint based on the kid (Key ID) found in the JWT header. This is specifically useful when using the RS256 algorithm.

    Common configuration options for expressJwtSecret include:

    • cache: Set to true to enable caching of signing keys to avoid repeated JWKS endpoint requests.
    • rateLimit: Set to true to enable rate limiting on JWKS requests.
    • jwksRequestsPerMinute: Defines the rate limit threshold.
    • jwksUri: The URL of your JWKS endpoint (e.g., https://my-authz-server/.well-known/jwks.json).
    const Express = require('express');
    const { expressjwt: jwt } = require('express-jwt');
    const jwksRsa = require('jwks-rsa');
    
    const app = new Express();
    app.use(jwt({
      secret: jwksRsa.expressJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://my-authz-server/.well-known/jwks.json`
      }),
      audience: 'urn:my-resource-server',
      issuer: 'https://my-authz-server/',
      algorithms: [ 'RS256' ]
    }));
  8. Run the Koa jwks-rsa demo

    master

    To run the Koa demonstration server, set the required environment variables and execute node server.js. You must provide the JWKS_HOST, AUDIENCE, and ISSUER values corresponding to your authorization server.

    Required Environment Variables:

    • JWKS_HOST: The base URL of your authorization server.
    • AUDIENCE: The expected audience of the JWT.
    • ISSUER: The expected issuer of the JWT.
    • DEBUG: (Optional) Set to koa,koa-jwt to see detailed logs.
    DEBUG=koa,koa-jwt JWKS_HOST=https://my-authz-server AUDIENCE=urn:my-resource-server ISSUER=https://my-authz-server/ node server.js
  9. Load keys from local files or environment variables using getKeysInterceptor

    master

    The getKeysInterceptor property allows you to fetch keys from an external source (like a local file or an environment variable) before the library attempts to call the jwksUri.

    If the kid is not found in the keys returned by the interceptor, the library falls back to the jwksUri endpoint. This works in conjunction with the LRU cache if enabled.

    const { JwksClient } = require('jwks-rsa');
    const fs = require('fs');
    
    const client = new JwksClient({
      jwksUri: 'https://my-enterprise-id-provider/.well-known/jwks.json',
      getKeysInterceptor: () => {
        const file = fs.readFileSync(jwksFile);
        return file.keys;
      }
    });
  10. Configure passport-jwt with jwks-rsa

    master

    The jwks-rsa library provides passportJwtSecret, a helper function that generates a secretOrKeyProvider for passport-jwt. This provider dynamically retrieves the correct signing key from a JWKS endpoint based on the kid (Key ID) found in the JWT header.

    To use it, pass the result of jwksRsa.passportJwtSecret() to the secretOrKeyProvider option in your JwtStrategy configuration. You should also specify the jwksUri and typically enable cache and rateLimit for production use.

    const JwtStrategy = require('passport-jwt').Strategy;
    const jwksRsa = require('jwks-rsa');
    
    passport.use(
      new JwtStrategy({
        // Dynamically provide a signing key based on the kid in the header
        secretOrKeyProvider: jwksRsa.passportJwtSecret({
          cache: true,
          rateLimit: true,
          jwksRequestsPerMinute: 5,
          jwksUri: `https://my-authz-server/.well-known/jwks.json`
        }),
        jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    
        // Validate the audience and the issuer.
        audience: 'urn:my-resource-server',
        issuer: 'https://my-authz-server/',
        algorithms: ['RS256']
      },
      verify)
    );