UNPKG

@theoptimalpartner/jwt-auth-validator

Version:

JWT token validation package with offline JWKS validation and Redis-based token revocation support

145 lines 5.14 kB
import jwt from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import NodeCache from 'node-cache'; import { hasClientSecret, safeCalculateSecretHash } from './cognito-utils.js'; export class JWKSService { keyCache; jwksClientInstance = null; config = null; constructor() { this.keyCache = new NodeCache({ stdTTL: 3600 }); } initialize(config) { this.config = { cacheTimeout: 3600, enableJWKSValidation: true, ...config, }; if (this.config.enableJWKSValidation && this.config.jwksUri) { this.jwksClientInstance = jwksClient({ jwksUri: this.config.jwksUri, cache: true, cacheMaxAge: (this.config.cacheTimeout || 3600) * 1000, rateLimit: true, jwksRequestsPerMinute: 10, }); } } static createCognitoConfig(region, userPoolId, clientId, clientSecret) { const config = { enableJWKSValidation: true, jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`, issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`, cacheTimeout: 3600, }; if (clientId) { config.audience = clientId; } if (clientSecret) { config.clientSecret = clientSecret; } return config; } async getPublicKey(kid) { return new Promise((resolve, reject) => { const cachedKey = this.keyCache.get(kid); if (cachedKey) { return resolve(cachedKey); } if (!this.jwksClientInstance) { return reject(new Error('JWKS client not initialized')); } this.jwksClientInstance.getSigningKey(kid, (err, key) => { if (err) { console.error('Error getting signing key:', err); return reject(new Error(`Failed to get public key for kid: ${kid}`)); } if (!key) { return reject(new Error(`No key found for kid: ${kid}`)); } try { const publicKey = key.getPublicKey(); this.keyCache.set(kid, publicKey); resolve(publicKey); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; reject(new Error(`Failed to get public key from signing key: ${errorMessage}`)); } }); }); } async validateTokenWithJWKS(token) { try { const decoded = jwt.decode(token, { complete: true }); if (!decoded?.header?.kid) { throw new Error('Token header missing or invalid'); } const header = decoded.header; const publicKey = await this.getPublicKey(header.kid); if (!this.config) { throw new Error('JWKS configuration missing'); } const verifyOptions = { algorithms: ['RS256'], issuer: this.config.issuer, audience: this.config.audience, clockTolerance: 60, }; return jwt.verify(token, publicKey, verifyOptions); } catch (error) { console.error('JWKS validation error:', error); throw error; } } async validateCognitoToken(token) { try { const decoded = await this.validateTokenWithJWKS(token); if (decoded && typeof decoded === 'object') { if (decoded.token_use && !['access', 'id'].includes(decoded.token_use)) { throw new Error(`Invalid token_use: ${decoded.token_use}`); } if (!decoded.sub) { throw new Error('Token missing sub claim'); } if (decoded.iss && !decoded.iss.includes('cognito-idp')) { throw new Error('Invalid issuer for Cognito token'); } } return decoded; } catch (error) { throw error; } } invalidateCache() { this.keyCache.flushAll(); } getCacheStats() { return { cache: this.keyCache.getStats(), config: this.config, }; } getClientSecret() { return this.config?.clientSecret; } hasClientSecret() { return hasClientSecret(this.config?.clientSecret); } calculateSecretHash(identifier) { if (!this.config?.audience || !this.config?.clientSecret) { return ""; } return safeCalculateSecretHash(identifier, this.config.audience, this.config.clientSecret); } reset() { this.jwksClientInstance = null; this.config = null; this.keyCache.flushAll(); } } //# sourceMappingURL=jwks-service.js.map