/** * Component: JWT Token Utilities * Documentation: documentation/backend/services/auth.md */ import jwt from 'jsonwebtoken'; import { RMABLogger } from './logger'; const logger = RMABLogger.create('JWT'); const JWT_SECRET = process.env.JWT_SECRET || 'change-this-to-a-random-secret-key'; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'change-this-to-another-random-secret-key'; const JWT_DOWNLOAD_SECRET = process.env.JWT_DOWNLOAD_SECRET || JWT_SECRET + '-download'; const ACCESS_TOKEN_EXPIRY = '1h'; // 1 hour const REFRESH_TOKEN_EXPIRY = '7d'; // 7 days export interface TokenPayload { sub: string; // User ID id: string; // User ID (alias for sub, used by req.user.id throughout the codebase) plexId: string; username: string; role: string; } export interface RefreshTokenPayload { sub: string; type: 'refresh'; } /** * Generate access token (short-lived) */ export function generateAccessToken(payload: TokenPayload): string { return jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY, }); } /** * Generate refresh token (long-lived) */ export function generateRefreshToken(userId: string): string { const payload: RefreshTokenPayload = { sub: userId, type: 'refresh', }; return jwt.sign(payload, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY, }); } /** * Verify access token */ export function verifyAccessToken(token: string): TokenPayload | null { try { const decoded = jwt.verify(token, JWT_SECRET) as TokenPayload; return decoded; } catch (error) { logger.error('Access token verification failed', { error: error instanceof Error ? error.message : String(error) }); return null; } } /** * Verify refresh token */ export function verifyRefreshToken(token: string): RefreshTokenPayload | null { try { const decoded = jwt.verify(token, JWT_REFRESH_SECRET) as RefreshTokenPayload; if (decoded.type !== 'refresh') { return null; } return decoded; } catch (error) { logger.error('Refresh token verification failed', { error: error instanceof Error ? error.message : String(error) }); return null; } } const DOWNLOAD_TOKEN_EXPIRY = '30d'; export interface DownloadTokenPayload { sub: string; // userId requestId: string; type: 'download'; } /** * Generate download token (30-day, stateless, URL-embeddable) */ export function generateDownloadToken(userId: string, requestId: string): string { const payload: DownloadTokenPayload = { sub: userId, requestId, type: 'download' }; return jwt.sign(payload, JWT_DOWNLOAD_SECRET, { expiresIn: DOWNLOAD_TOKEN_EXPIRY }); } /** * Verify download token */ export function verifyDownloadToken(token: string): DownloadTokenPayload | null { try { const decoded = jwt.verify(token, JWT_DOWNLOAD_SECRET) as DownloadTokenPayload; if (decoded.type !== 'download') return null; return decoded; } catch { return null; } } /** * Decode token without verification (for debugging) */ export function decodeToken(token: string): any { try { return jwt.decode(token); } catch (error) { return null; } }