@theoptimalpartner/jwt-auth-validator
Version:
JWT token validation package with offline JWKS validation and Redis-based token revocation support
251 lines • 8.46 kB
JavaScript
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