All files / backend/src/auth token-service.ts

79.16% Statements 19/24
72.72% Branches 8/11
71.42% Functions 5/7
82.6% Lines 19/23

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75                                      10x     10x 10x 10x     10x       6x 6x       2x 2x       10x 2x     8x   8x 1x     5x       2x 2x 2x 2x                                
/**
 * JWT Token Service
 *
 * Handles token issuance, verification, and blacklisting.
 * Uses short-lived access tokens + longer-lived refresh tokens.
 */
 
import jwt, { type SignOptions } from 'jsonwebtoken';
 
interface TokenPayload {
  userId: string;
  email: string;
  type: 'access' | 'refresh';
}
 
export class TokenService {
  private secret: string;
  private accessTokenExpiry: string;
  private refreshTokenExpiry: string;
  private blacklist: Map<string, number> = new Map(); // token -> expiry timestamp
 
  constructor(secret: string, accessTokenExpiry: string = '15m', refreshTokenExpiry: string = '7d') {
    this.secret = secret;
    this.accessTokenExpiry = accessTokenExpiry;
    this.refreshTokenExpiry = refreshTokenExpiry;
 
    // Clean up expired blacklist entries every 5 minutes
    setInterval(() => this.cleanupBlacklist(), 5 * 60 * 1000).unref();
  }
 
  issueAccessToken(userId: string, email: string): string {
    const payload: TokenPayload = { userId, email, type: 'access' };
    return jwt.sign(payload, this.secret, { expiresIn: this.accessTokenExpiry } as SignOptions);
  }
 
  issueRefreshToken(userId: string, email: string): string {
    const payload: TokenPayload = { userId, email, type: 'refresh' };
    return jwt.sign(payload, this.secret, { expiresIn: this.refreshTokenExpiry } as SignOptions);
  }
 
  verifyToken(token: string, expectedType: 'access' | 'refresh' = 'access'): TokenPayload {
    if (this.blacklist.has(token)) {
      throw new Error('Token has been revoked');
    }
 
    const decoded = jwt.verify(token, this.secret) as TokenPayload & { exp: number };
 
    if (decoded.type !== expectedType) {
      throw new Error(`Expected ${expectedType} token, got ${decoded.type}`);
    }
 
    return decoded;
  }
 
  blacklistToken(token: string): void {
    try {
      const decoded = jwt.decode(token) as { exp?: number } | null;
      Eif (decoded?.exp) {
        this.blacklist.set(token, decoded.exp * 1000); // Convert to ms
      }
    } catch {
      // If we can't decode it, no need to blacklist
    }
  }
 
  private cleanupBlacklist(): void {
    const now = Date.now();
    for (const [token, expiry] of this.blacklist) {
      if (expiry < now) {
        this.blacklist.delete(token);
      }
    }
  }
}