UNPKG

@theoptimalpartner/jwt-auth-validator

Version:

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

251 lines 8.46 kB
import jwt from 'jsonwebtoken'; import { JWKSService } from './jwks-service.js'; import { TokenBlacklistService } from './token-blacklist-service.js'; import { RedisService } from './redis-service.js'; export class JWTValidator { jwksService; blacklistService; redisService; config; initialized = false; constructor(config) { this.config = config; this.jwksService = new JWKSService(); if (config.enableRedisBlacklist && config.redis) { this.redisService = new RedisService(config.redis); this.blacklistService = new TokenBlacklistService(this.redisService); } } async initialize() { if (this.initialized) return; try { this.jwksService.initialize(this.config.jwks); if (this.redisService && this.blacklistService) { await this.redisService.initialize(); this.blacklistService.initialize(this.redisService); } this.initialized = true; } catch (error) { console.error('Failed to initialize JWT Validator:', error); throw error; } } async validateToken(token, forceSecure = false) { try { if (!this.initialized) { await this.initialize(); } const useSecureValidation = forceSecure || this.config.forceSecureValidation || process.env.NODE_ENV === 'production'; if (useSecureValidation) { return await this.validateTokenSecure(token); } else { console.warn('Using basic token validation. This should not be used in production!'); return await this.validateTokenBasic(token); } } catch (error) { console.error('Token validation failed:', error); return { valid: false, error: 'Token validation failed', }; } } async validateTokenSecure(token) { try { if (!this.initialized) { await this.initialize(); } if (this.blacklistService) { const isBlacklisted = await this.blacklistService.isBlacklisted(token); if (isBlacklisted) { return { valid: false, error: 'Token has been revoked', }; } } const decoded = await this.jwksService.validateCognitoToken(token); return { valid: true, decoded: decoded, }; } catch (error) { console.error('Secure token validation failed:', error); let errorMessage = 'Token validation failed'; if (typeof error === 'object' && error !== null && 'name' in error) { const err = error; if (err.name === 'TokenExpiredError') { errorMessage = 'Token expired'; } else if (err.name === 'JsonWebTokenError') { errorMessage = 'Invalid token format'; } else if (err.name === 'NotBeforeError') { errorMessage = 'Token not active yet'; } else if (err.message?.includes('kid')) { errorMessage = 'Invalid token signature'; } } return { valid: false, error: errorMessage, }; } } async validateTokenBasic(token) { try { if (this.blacklistService) { const isBlacklisted = await this.blacklistService.isBlacklisted(token); if (isBlacklisted) { return { valid: false, error: 'Token has been revoked', }; } } const decoded = jwt.decode(token); if (!decoded) { return { valid: false, error: 'Invalid token format' }; } const currentTime = Math.floor(Date.now() / 1000); if (decoded.exp && decoded.exp < currentTime) { return { valid: false, error: 'Token expired' }; } if (!decoded.sub) { return { valid: false, error: 'Token missing sub claim' }; } return { valid: true, decoded }; } catch { return { valid: false, error: 'Token validation failed' }; } } async validateAccessToken(token) { const result = await this.validateToken(token); if (!result.valid || !result.decoded) { return result; } if (result.decoded.token_use !== 'access') { return { valid: false, error: `Invalid token type. Expected 'access', got '${result.decoded.token_use || 'unknown'}'`, }; } return result; } async validateIdToken(token) { const result = await this.validateToken(token); if (!result.valid || !result.decoded) { return result; } if (result.decoded.token_use !== 'id') { return { valid: false, error: `Invalid token type. Expected 'id', got '${result.decoded.token_use || 'unknown'}'`, }; } return result; } extractTokenFromHeader(authHeader) { if (!authHeader?.startsWith('Bearer ')) { return null; } return authHeader.substring(7); } getTokenInfo(token) { try { return jwt.decode(token, { complete: true }); } catch { return null; } } isTokenExpired(token) { try { const decoded = jwt.decode(token); if (!decoded?.exp) return true; const currentTime = Math.floor(Date.now() / 1000); return decoded.exp < currentTime; } catch { return true; } } getTimeToExpiry(token) { try { const decoded = jwt.decode(token); if (!decoded?.exp) return 0; const currentTime = Math.floor(Date.now() / 1000); return Math.max(0, decoded.exp - currentTime); } catch { return 0; } } async validateMultipleTokens(tokens) { const promises = tokens.map((token) => this.validateToken(token)); return await Promise.all(promises); } decodeToken(token) { try { return jwt.decode(token); } catch { return null; } } async revokeToken(token) { if (!this.blacklistService) { throw new Error('Redis blacklist not enabled'); } const decoded = this.decodeToken(token); if (!decoded?.exp) { throw new Error('Cannot revoke token without expiration'); } await this.blacklistService.addToBlacklist(token, decoded.exp); } async revokeUserTokens(userId, tokens) { if (!this.blacklistService) { throw new Error('Redis blacklist not enabled'); } await this.blacklistService.invalidateUserTokens(userId, tokens); } getCacheStats() { return this.jwksService.getCacheStats(); } async getBlacklistStats() { if (!this.blacklistService) { return null; } return await this.blacklistService.getStats(); } getClientSecret() { return this.config.jwks.clientSecret; } hasClientSecret() { return Boolean(this.config.jwks.clientSecret && this.config.jwks.clientSecret.trim().length > 0); } calculateSecretHash(identifier) { if (!this.config.jwks.clientSecret || !this.config.jwks.audience) { return ""; } const { safeCalculateSecretHash } = require('./cognito-utils.js'); return safeCalculateSecretHash(identifier, this.config.jwks.audience, this.config.jwks.clientSecret); } async disconnect() { if (this.redisService) { await this.redisService.disconnect(); } } } //# sourceMappingURL=jwt-validator.js.map