@theoptimalpartner/jwt-auth-validator
Version:
JWT token validation package with offline JWKS validation and Redis-based token revocation support
137 lines • 4.58 kB
JavaScript
import jwt from 'jsonwebtoken';
export class TokenBlacklistService {
redisService = null;
initialized = false;
constructor(redisService) {
if (redisService) {
this.redisService = redisService;
}
}
initialize(redisService) {
if (redisService) {
this.redisService = redisService;
}
if (!this.redisService) {
throw new Error('Redis service is required for token blacklist');
}
this.initialized = true;
}
async addToBlacklist(token, expiresAt) {
if (!this.initialized || !this.redisService) {
throw new Error('TokenBlacklistService not initialized');
}
try {
const currentTime = Math.floor(Date.now() / 1000);
const ttl = Math.max(0, expiresAt - currentTime);
if (ttl > 0) {
const tokenHash = this.getTokenHash(token);
await this.redisService.saveRevokedToken(tokenHash, ttl);
}
}
catch (error) {
console.error('Error adding token to blacklist:', error);
throw error;
}
}
async isBlacklisted(token) {
if (!this.initialized || !this.redisService) {
return false;
}
try {
const tokenHash = this.getTokenHash(token);
return await this.redisService.isTokenRevoked(tokenHash);
}
catch (error) {
console.error('Error checking blacklist:', error);
return false;
}
}
getTokenId(token) {
try {
const decoded = jwt.decode(token);
if (decoded?.jti) {
return decoded.jti;
}
if (decoded?.sub && decoded.iat) {
return `${decoded.sub}:${decoded.iat}`;
}
return this.getTokenHash(token);
}
catch (error) {
console.error('Error getting token ID:', error);
return this.getTokenHash(token);
}
}
getTokenHash(token) {
const tokenSuffix = token.slice(-32);
return Buffer.from(tokenSuffix).toString('base64');
}
async invalidateUserTokens(userId, tokens) {
if (!this.initialized || !this.redisService) {
throw new Error('TokenBlacklistService not initialized');
}
try {
const promises = tokens.map(async (token) => {
try {
const decoded = jwt.decode(token);
if (decoded?.exp) {
await this.addToBlacklist(token, decoded.exp);
}
}
catch (error) {
console.error(`Error invalidating token for user ${userId}:`, error);
}
});
await Promise.all(promises);
}
catch (error) {
console.error('Error invalidating user tokens:', error);
throw error;
}
}
async addMultipleToBlacklist(tokens) {
if (!this.initialized || !this.redisService) {
throw new Error('TokenBlacklistService not initialized');
}
try {
const promises = tokens.map(({ token, expiresAt }) => this.addToBlacklist(token, expiresAt));
await Promise.all(promises);
}
catch (error) {
console.error('Error adding multiple tokens to blacklist:', error);
throw error;
}
}
async checkMultipleTokens(tokens) {
if (!this.initialized || !this.redisService) {
return tokens.map(() => false);
}
try {
const promises = tokens.map((token) => this.isBlacklisted(token));
return await Promise.all(promises);
}
catch (error) {
console.error('Error checking multiple tokens:', error);
return tokens.map(() => false);
}
}
async getStats() {
try {
const connectionStatus = this.redisService?.isConnected ? 'connected' : 'disconnected';
return {
service: 'Redis',
connectionStatus,
initialized: this.initialized,
};
}
catch (error) {
return {
service: 'Redis',
connectionStatus: 'error',
error: error instanceof Error ? error.message : String(error),
initialized: this.initialized,
};
}
}
}
//# sourceMappingURL=token-blacklist-service.js.map