# jwks-rsa jwks-rsa is a Node.js library that retrieves RSA signing keys from a JWKS (JSON Web Key Set) endpoint. It enables applications to dynamically fetch public keys for JWT (JSON Web Token) validation without hardcoding certificates, which is essential for handling key rotation in OAuth 2.0 and OpenID Connect implementations. The library supports RSA, EC, and OKP key types with automatic algorithm detection. The library provides built-in integrations for popular Node.js web frameworks including Express, Koa, Hapi, and Passport.js. It features intelligent caching with LRU eviction, rate limiting to prevent abuse from malicious key ID requests, and support for custom fetchers, proxies, and TLS/SSL configurations. This makes it ideal for securing APIs that need to validate JWTs from identity providers like Auth0, Okta, or custom OAuth servers. ## Creating a JWKS Client The JwksClient is the core class that fetches and manages signing keys from a JWKS endpoint. It accepts configuration options for caching, rate limiting, timeouts, and custom request handling. ```javascript const jwksClient = require('jwks-rsa'); // Create a client with default settings const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' }); // Create a client with full configuration const clientWithOptions = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', cache: true, // Enable caching (default: true) cacheMaxEntries: 5, // Max keys to cache (default: 5) cacheMaxAge: 600000, // Cache TTL in ms (default: 600000 = 10 minutes) rateLimit: true, // Enable rate limiting (default: false) jwksRequestsPerMinute: 10, // Max requests per minute (default: 10) timeout: 30000, // Request timeout in ms (default: 30000) requestHeaders: { // Custom headers 'User-Agent': 'my-app/1.0' } }); ``` ## Retrieving a Signing Key by Key ID (kid) The getSigningKey method fetches a specific signing key by its key ID (kid). This is the primary method used during JWT verification to obtain the public key matching the token's header. ```javascript const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', cache: true, rateLimit: true }); async function verifyToken(token) { const jwt = require('jsonwebtoken'); // Decode the token header to get the kid const decoded = jwt.decode(token, { complete: true }); const kid = decoded.header.kid; try { // Fetch the signing key const key = await client.getSigningKey(kid); // Get the public key in PEM format const signingKey = key.getPublicKey(); // Alternative: key.publicKey or key.rsaPublicKey // Verify the token const verified = jwt.verify(token, signingKey, { algorithms: ['RS256', 'ES256', 'EdDSA'] }); return verified; } catch (err) { if (err.name === 'SigningKeyNotFoundError') { console.error('No matching key found for kid:', kid); } else if (err.name === 'JwksRateLimitError') { console.error('Rate limit exceeded'); } throw err; } } // Callback-style usage (also supported) client.getSigningKey(kid, (err, key) => { if (err) { console.error('Error fetching key:', err); return; } const signingKey = key.getPublicKey(); }); ``` ## Retrieving All Signing Keys The getSigningKeys method returns all valid signing keys from the JWKS endpoint. Keys are filtered to include only those with `use: "sig"` or no use specified, and must be RSA, EC, or OKP key types. ```javascript const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' }); async function getAllKeys() { try { const keys = await client.getSigningKeys(); console.log(`Found ${keys.length} signing keys:`); keys.forEach(key => { console.log(` - kid: ${key.kid}, alg: ${key.alg}`); console.log(` Public Key: ${key.getPublicKey().substring(0, 50)}...`); }); return keys; } catch (err) { if (err.message === 'The JWKS endpoint did not contain any signing keys') { console.error('No valid signing keys found'); } throw err; } } // Expected output: // Found 2 signing keys: // - kid: ABC123, alg: RS256 // Public Key: -----BEGIN PUBLIC KEY-----... // - kid: DEF456, alg: ES256 // Public Key: -----BEGIN PUBLIC KEY-----... ``` ## Express.js Integration with express-jwt The expressJwtSecret helper integrates with express-jwt middleware to automatically provide signing keys for JWT verification. It supports both express-jwt v6 and v7+. ```javascript const express = require('express'); const { expressjwt: jwt } = require('express-jwt'); const { expressJwtSecret } = require('jwks-rsa'); const app = express(); // Configure JWT middleware with JWKS app.use(jwt({ secret: expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 10, jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' }), audience: 'https://api.example.com', issuer: 'https://your-domain.auth0.com/', algorithms: ['RS256'] })); // Protected route - req.auth contains the decoded JWT payload app.get('/api/protected', (req, res) => { res.json({ message: 'Access granted', user: req.auth.sub, scopes: req.auth.scope }); }); // Error handling middleware app.use((err, req, res, next) => { if (err.name === 'UnauthorizedError') { return res.status(401).json({ error: 'Invalid token', message: err.message }); } next(err); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); // Test with curl: // curl -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:3000/api/protected ``` ## Koa.js Integration with koa-jwt The koaJwtSecret helper integrates with koa-jwt middleware for JWT verification in Koa applications. ```javascript const Koa = require('koa'); const Router = require('koa-router'); const jwt = require('koa-jwt'); const jwksRsa = require('jwks-rsa'); const app = new Koa(); const router = new Router(); // Configure JWT middleware with JWKS app.use(jwt({ secret: jwksRsa.koaJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 10, jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', // Custom error handler (optional) handleSigningKeyError: async (err) => { console.error('Signing key error:', err.message); } }), audience: 'https://api.example.com', issuer: 'https://your-domain.auth0.com/', algorithms: ['RS256'] })); // Protected routes - ctx.state.user contains decoded JWT router.get('/api/me', ctx => { ctx.body = { userId: ctx.state.user.sub, email: ctx.state.user.email, permissions: ctx.state.user.permissions }; }); router.get('/api/data', ctx => { ctx.body = { data: 'sensitive information', user: ctx.state.user.sub }; }); app.use(router.routes()); app.use(router.allowedMethods()); app.listen(3000); ``` ## Passport.js Integration with passport-jwt The passportJwtSecret helper integrates with passport-jwt strategy for JWT authentication in Express applications using Passport.js. ```javascript const express = require('express'); const passport = require('passport'); const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); const jwksRsa = require('jwks-rsa'); const app = express(); // Configure Passport JWT strategy with JWKS passport.use(new JwtStrategy({ // Dynamically provide signing key based on kid in token header secretOrKeyProvider: jwksRsa.passportJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 10, jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' }), // Extract JWT from Authorization header as Bearer token jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Validate audience and issuer audience: 'https://api.example.com', issuer: 'https://your-domain.auth0.com/', algorithms: ['RS256'] }, (jwtPayload, done) => { // Verify callback - validate the user if (jwtPayload && jwtPayload.sub) { return done(null, { id: jwtPayload.sub, email: jwtPayload.email, roles: jwtPayload['https://example.com/roles'] || [] }); } return done(null, false); })); app.use(passport.initialize()); // Protected route using Passport authentication app.get('/api/profile', passport.authenticate('jwt', { session: false }), (req, res) => { res.json({ message: 'Authenticated successfully', user: req.user }); } ); // Error handling app.use((err, req, res, next) => { console.error('Auth error:', err.message); res.status(401).json({ error: 'Authentication failed' }); }); app.listen(3000); ``` ## Hapi.js Integration with hapi-auth-jwt2 The hapiJwt2KeyAsync helper integrates with hapi-auth-jwt2 plugin for JWT authentication in Hapi applications. ```javascript const Hapi = require('@hapi/hapi'); const jwt = require('hapi-auth-jwt2'); const jwksRsa = require('jwks-rsa'); const init = async () => { const server = Hapi.server({ port: 3000, host: 'localhost' }); // Register JWT plugin await server.register(jwt); // Configure JWT strategy with JWKS server.auth.strategy('jwt', 'jwt', { complete: true, headerKey: 'authorization', tokenType: 'Bearer', // Use async key provider from jwks-rsa key: jwksRsa.hapiJwt2KeyAsync({ cache: true, rateLimit: true, jwksRequestsPerMinute: 10, jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' }), // Validate function validate: async (decoded, request, h) => { // decoded contains the JWT payload if (decoded && decoded.sub) { return { isValid: true, credentials: { userId: decoded.sub, scope: decoded.scope ? decoded.scope.split(' ') : [] } }; } return { isValid: false }; }, verifyOptions: { audience: 'https://api.example.com', issuer: 'https://your-domain.auth0.com/', algorithms: ['RS256'] } }); server.auth.default('jwt'); // Protected routes server.route([ { method: 'GET', path: '/api/me', handler: (request, h) => { return { userId: request.auth.credentials.userId, scope: request.auth.credentials.scope }; } }, { method: 'GET', path: '/api/admin', options: { auth: { strategy: 'jwt', scope: ['admin'] // Requires admin scope } }, handler: (request, h) => { return { message: 'Admin access granted' }; } } ]); await server.start(); console.log('Server running on %s', server.info.uri); }; init(); ``` ## Custom TLS/SSL Configuration The requestAgent option allows configuring custom HTTPS agents for TLS/SSL settings, useful for enterprise environments with private certificate authorities. ```javascript const jwksClient = require('jwks-rsa'); const https = require('https'); const fs = require('fs'); // Configure with custom CA certificate const client = jwksClient({ jwksUri: 'https://internal-idp.company.com/.well-known/jwks.json', requestAgent: new https.Agent({ // Custom CA certificate for internal PKI ca: fs.readFileSync('/path/to/ca-certificate.pem'), // Client certificate authentication (mTLS) cert: fs.readFileSync('/path/to/client-cert.pem'), key: fs.readFileSync('/path/to/client-key.pem'), // Additional TLS options rejectUnauthorized: true, minVersion: 'TLSv1.2' }), timeout: 10000 }); // Using with proxy (requires https-proxy-agent) const HttpsProxyAgent = require('https-proxy-agent'); const clientWithProxy = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', requestAgent: new HttpsProxyAgent('http://proxy.company.com:8080') }); async function getKey(kid) { const key = await client.getSigningKey(kid); return key.getPublicKey(); } ``` ## Loading Keys from Local Sources with getKeysInterceptor The getKeysInterceptor option allows loading keys from local files, environment variables, or external caches before falling back to the JWKS endpoint. ```javascript const jwksClient = require('jwks-rsa'); const fs = require('fs'); // Load keys from a local file with fallback to remote JWKS const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', cache: true, getKeysInterceptor: async () => { // Try to load from local file first try { const localJwks = JSON.parse( fs.readFileSync('/etc/app/jwks.json', 'utf8') ); console.log('Loaded keys from local file'); return localJwks.keys; } catch (err) { console.log('Local keys not found, will fetch from remote'); return []; // Empty array triggers fallback to jwksUri } } }); // Load keys from environment variable const clientFromEnv = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', getKeysInterceptor: async () => { if (process.env.JWKS_KEYS) { const jwks = JSON.parse(process.env.JWKS_KEYS); return jwks.keys; } return []; } }); // Load keys from Redis cache const Redis = require('ioredis'); const redis = new Redis(); const clientWithRedis = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', getKeysInterceptor: async () => { const cached = await redis.get('jwks:keys'); if (cached) { return JSON.parse(cached); } return []; } }); async function verifyWithLocalKeys(token, kid) { const key = await client.getSigningKey(kid); return key.getPublicKey(); } ``` ## Custom Fetcher Function The fetcher option allows providing a custom function to fetch JWKS data, useful for testing or when additional request handling is needed. ```javascript const jwksClient = require('jwks-rsa'); const fetch = require('node-fetch'); // Custom fetcher with retry logic const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', fetcher: async (jwksUri) => { const maxRetries = 3; let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(jwksUri, { headers: { 'Accept': 'application/json', 'User-Agent': 'my-app/1.0' }, timeout: 5000 }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data; // Must return { keys: [...] } } catch (err) { lastError = err; console.log(`JWKS fetch attempt ${attempt} failed:`, err.message); if (attempt < maxRetries) { await new Promise(r => setTimeout(r, 1000 * attempt)); } } } throw lastError; } }); // Fetcher for testing with mock keys const testClient = jwksClient({ fetcher: async () => ({ keys: [{ kty: 'RSA', kid: 'test-key-1', use: 'sig', n: 'base64-encoded-modulus', e: 'AQAB' }] }) }); ``` ## Error Handling The library exports specific error classes for different failure scenarios, enabling precise error handling in applications. ```javascript const jwksClient = require('jwks-rsa'); const { ArgumentError, JwksError, JwksRateLimitError, SigningKeyNotFoundError } = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json', cache: true, rateLimit: true, jwksRequestsPerMinute: 5 }); async function getKeyWithErrorHandling(kid) { try { const key = await client.getSigningKey(kid); return key.getPublicKey(); } catch (err) { if (err instanceof SigningKeyNotFoundError) { // No key matches the provided kid console.error(`Key not found: ${kid}`); console.error('This may indicate key rotation - token may be expired'); throw new Error('TOKEN_KEY_NOT_FOUND'); } if (err instanceof JwksRateLimitError) { // Too many requests to the JWKS endpoint console.error('Rate limit exceeded for JWKS endpoint'); console.error('Possible attack with random kid values'); throw new Error('RATE_LIMIT_EXCEEDED'); } if (err instanceof JwksError) { // General JWKS error (network, invalid response, etc.) console.error('JWKS endpoint error:', err.message); throw new Error('JWKS_UNAVAILABLE'); } if (err instanceof ArgumentError) { // Invalid configuration console.error('Configuration error:', err.message); throw err; } // Unknown error console.error('Unexpected error:', err); throw err; } } // Express error middleware example function jwtErrorHandler(err, req, res, next) { if (err.message === 'RATE_LIMIT_EXCEEDED') { return res.status(429).json({ error: 'Too many requests' }); } if (err.message === 'TOKEN_KEY_NOT_FOUND') { return res.status(401).json({ error: 'Invalid token - key not found' }); } if (err.message === 'JWKS_UNAVAILABLE') { return res.status(503).json({ error: 'Authentication service unavailable' }); } next(err); } ``` ## Enabling Debug Logs Debug logging can be enabled via the DEBUG environment variable to troubleshoot key retrieval issues. ```bash # Enable jwks-rsa debug logs DEBUG=jwks node app.js # Enable all debug logs DEBUG=* node app.js # Enable multiple debug namespaces DEBUG=jwks,express:* node app.js ``` ```javascript // Example debug output when fetching keys: // jwks Fetching keys from 'https://your-domain.auth0.com/.well-known/jwks.json' +0ms // jwks Keys: +150ms [ // { kty: 'RSA', use: 'sig', kid: 'ABC123', alg: 'RS256', n: '...', e: 'AQAB' }, // { kty: 'RSA', use: 'sig', kid: 'DEF456', alg: 'RS256', n: '...', e: 'AQAB' } // ] // jwks Signing Keys: +5ms [ // { kid: 'ABC123', alg: 'RS256', publicKey: '-----BEGIN PUBLIC KEY-----...' } // ] // jwks Configured caching of signing keys. Max: 5 / Age: 600000 +0ms // jwks Configured rate limiting to JWKS endpoint at 10/minute +0ms ``` ## Summary jwks-rsa is the go-to solution for Node.js applications that need to validate JWTs from OAuth 2.0 and OpenID Connect providers. Its primary use cases include securing REST APIs with dynamically-fetched public keys, integrating with identity providers like Auth0, Okta, and Azure AD, and implementing token-based authentication in microservices architectures. The library handles the complexity of key management, caching, and rotation automatically, allowing developers to focus on application logic rather than cryptographic infrastructure. The library's integration patterns follow established conventions for each framework ecosystem. Express applications use the expressJwtSecret helper with express-jwt middleware, Koa applications use koaJwtSecret with koa-jwt, Hapi applications use hapiJwt2KeyAsync with hapi-auth-jwt2, and Passport.js applications use passportJwtSecret with passport-jwt strategy. All integrations support the same configuration options for caching, rate limiting, and error handling, making it straightforward to implement consistent authentication across different Node.js frameworks or migrate between them.