/** * Token Generator - Secure Token Generation * * Provides cryptographically secure token generation for: * - JWT tokens * - Session tokens * - CSRF tokens * - API tokens * - Verification codes * * Security Properties: * - Uses crypto.randomBytes for all randomness * - Configurable entropy levels * - Timing-safe comparison * - Token expiration handling * * @module v3/security/token-generator */ import { randomBytes, createHmac, timingSafeEqual } from 'crypto'; export class TokenGeneratorError extends Error { code; constructor(message, code) { super(message); this.code = code; this.name = 'TokenGeneratorError'; } } /** * Secure token generator. * * @example * ```typescript * const generator = new TokenGenerator({ hmacSecret: 'secret' }); * * // Generate session token * const session = generator.generateSessionToken(); * * // Generate signed token * const signed = generator.generateSignedToken({ userId: '123' }); * * // Verify signed token * const isValid = generator.verifySignedToken(signed.combined); * ``` */ export class TokenGenerator { config; constructor(config = {}) { this.config = { defaultLength: config.defaultLength ?? 32, encoding: config.encoding ?? 'base64url', hmacSecret: config.hmacSecret ?? '', defaultExpiration: config.defaultExpiration ?? 3600, }; if (this.config.defaultLength < 16) { throw new TokenGeneratorError('Token length must be at least 16 bytes', 'INVALID_LENGTH'); } } /** * Generates a random token. * * @param length - Token length in bytes * @returns Random token string */ generate(length) { const len = length ?? this.config.defaultLength; const buffer = randomBytes(len); return this.encode(buffer); } /** * Generates a token with expiration. * * @param expirationSeconds - Expiration time in seconds * @param metadata - Optional metadata to attach * @returns Token with expiration */ generateWithExpiration(expirationSeconds, metadata) { const expiration = expirationSeconds ?? this.config.defaultExpiration; const now = new Date(); const expiresAt = new Date(now.getTime() + expiration * 1000); return { value: this.generate(), createdAt: now, expiresAt, metadata, }; } /** * Generates a session token (URL-safe). * * @param length - Token length in bytes (default: 32) * @returns Session token */ generateSessionToken(length = 32) { return this.generateWithExpiration(this.config.defaultExpiration); } /** * Generates a CSRF token. * * @returns CSRF token (shorter expiration) */ generateCsrfToken() { return this.generateWithExpiration(1800); // 30 minutes } /** * Generates an API token with prefix. * * @param prefix - Token prefix (e.g., 'cf_') * @returns Prefixed API token */ generateApiToken(prefix = 'cf_') { const tokenBody = this.generate(32); const now = new Date(); return { value: `${prefix}${tokenBody}`, createdAt: now, expiresAt: new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000), // 1 year }; } /** * Generates a numeric verification code. * * @param length - Number of digits (default: 6) * @param expirationMinutes - Expiration in minutes (default: 10) * @param maxAttempts - Maximum verification attempts (default: 3) * @returns Verification code */ generateVerificationCode(length = 6, expirationMinutes = 10, maxAttempts = 3) { const buffer = randomBytes(length); let code = ''; for (let i = 0; i < length; i++) { code += (buffer[i] % 10).toString(); } const now = new Date(); return { code, createdAt: now, expiresAt: new Date(now.getTime() + expirationMinutes * 60 * 1000), attempts: 0, maxAttempts, }; } /** * Generates a signed token using HMAC. * * @param payload - Data to include in token * @param expirationSeconds - Token expiration * @returns Signed token */ generateSignedToken(payload, expirationSeconds) { if (!this.config.hmacSecret) { throw new TokenGeneratorError('HMAC secret required for signed tokens', 'NO_SECRET'); } const expiration = expirationSeconds ?? this.config.defaultExpiration; const now = new Date(); const expiresAt = new Date(now.getTime() + expiration * 1000); const tokenData = { ...payload, iat: now.getTime(), exp: expiresAt.getTime(), nonce: this.generate(8), }; const token = Buffer.from(JSON.stringify(tokenData)).toString('base64url'); const signature = this.sign(token); return { token, signature, combined: `${token}.${signature}`, createdAt: now, expiresAt, }; } /** * Verifies a signed token. * * @param combined - Combined token string (token.signature) * @returns Decoded payload if valid, null otherwise */ verifySignedToken(combined) { if (!this.config.hmacSecret) { throw new TokenGeneratorError('HMAC secret required for signed tokens', 'NO_SECRET'); } const parts = combined.split('.'); if (parts.length !== 2) { return null; } const [token, signature] = parts; // Verify signature const expectedSignature = this.sign(token); try { const sigBuffer = Buffer.from(signature, 'base64url'); const expectedBuffer = Buffer.from(expectedSignature, 'base64url'); if (sigBuffer.length !== expectedBuffer.length) { return null; } if (!timingSafeEqual(sigBuffer, expectedBuffer)) { return null; } } catch { return null; } // Decode and validate payload try { const payload = JSON.parse(Buffer.from(token, 'base64url').toString()); // Check expiration if (payload.exp && payload.exp < Date.now()) { return null; } return payload; } catch { return null; } } /** * Generates a refresh token pair. * * @returns Access and refresh tokens */ generateTokenPair() { return { accessToken: this.generateWithExpiration(900), // 15 minutes refreshToken: this.generateWithExpiration(604800), // 7 days }; } /** * Generates a password reset token. * * @returns Password reset token (short expiration) */ generatePasswordResetToken() { return this.generateWithExpiration(1800); // 30 minutes } /** * Generates an email verification token. * * @returns Email verification token */ generateEmailVerificationToken() { return this.generateWithExpiration(86400); // 24 hours } /** * Generates a unique request ID. * * @returns Request ID (shorter, for logging) */ generateRequestId() { return this.generate(8); } /** * Generates a correlation ID for distributed tracing. * * @returns Correlation ID */ generateCorrelationId() { const timestamp = Date.now().toString(36); const random = this.generate(8); return `${timestamp}-${random}`; } /** * Checks if a token has expired. * * @param token - Token to check * @returns True if expired */ isExpired(token) { return token.expiresAt < new Date(); } /** * Compares two tokens in constant time. * * @param a - First token * @param b - Second token * @returns True if equal */ compare(a, b) { if (a.length !== b.length) { return false; } try { const bufferA = Buffer.from(a); const bufferB = Buffer.from(b); return timingSafeEqual(bufferA, bufferB); } catch { return false; } } /** * Signs data using HMAC-SHA256. */ sign(data) { return createHmac('sha256', this.config.hmacSecret) .update(data) .digest('base64url'); } /** * Encodes bytes according to configuration. */ encode(buffer) { switch (this.config.encoding) { case 'hex': return buffer.toString('hex'); case 'base64': return buffer.toString('base64'); case 'base64url': default: return buffer.toString('base64url'); } } } /** * Factory function to create a production token generator. * * @param hmacSecret - HMAC secret for signed tokens * @returns Configured TokenGenerator */ export function createTokenGenerator(hmacSecret) { return new TokenGenerator({ hmacSecret, defaultLength: 32, encoding: 'base64url', }); } /** * Singleton instance for quick token generation without configuration. */ let defaultGenerator = null; /** * Gets or creates the default token generator. * Note: Does not support signed tokens without configuration. */ export function getDefaultGenerator() { if (!defaultGenerator) { defaultGenerator = new TokenGenerator(); } return defaultGenerator; } /** * Quick token generation functions. */ export const quickGenerate = { token: (length = 32) => getDefaultGenerator().generate(length), sessionToken: () => getDefaultGenerator().generateSessionToken(), csrfToken: () => getDefaultGenerator().generateCsrfToken(), apiToken: (prefix = 'cf_') => getDefaultGenerator().generateApiToken(prefix), verificationCode: (length = 6) => getDefaultGenerator().generateVerificationCode(length), requestId: () => getDefaultGenerator().generateRequestId(), correlationId: () => getDefaultGenerator().generateCorrelationId(), }; //# sourceMappingURL=token-generator.js.map