UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

153 lines 5.45 kB
/** * CSRF Token Generator * Handles generation and validation of CSRF tokens */ import { createHash, randomBytes, timingSafeEqual } from 'crypto'; export class CSRFTokenGenerator { config; constructor(config) { this.config = { secret: config.secret, tokenExpiry: config.tokenExpiry || 60 * 60 * 1000, // 1 hour default cookieName: config.cookieName || '__csrf-token', headerName: config.headerName || 'X-CSRF-Token', fieldName: config.fieldName || '_csrf', secureCookie: config.secureCookie ?? true, httpOnlyCookie: config.httpOnlyCookie ?? true, sameSite: config.sameSite || 'strict' }; } /** * Generate a new CSRF token for a session */ generateToken(sessionId) { const timestamp = Date.now(); const expiresAt = timestamp + this.config.tokenExpiry; // Generate random bytes for the token const randomData = randomBytes(32); // Create token payload const payload = JSON.stringify({ sessionId, timestamp, expiresAt, random: randomData.toString('hex') }); // Sign the payload with HMAC const signature = this.signPayload(payload); // Combine payload and signature const tokenValue = Buffer.from(payload).toString('base64') + '.' + signature; return { value: tokenValue, expiresAt, sessionId }; } /** * Validate a CSRF token */ validateToken(tokenValue, sessionId) { try { // Split token into payload and signature const parts = tokenValue.split('.'); if (parts.length !== 2) { return { valid: false, error: 'Invalid token format' }; } const [payloadBase64, signature] = parts; if (!payloadBase64 || !signature) { return { valid: false, error: 'Invalid token format' }; } const payload = Buffer.from(payloadBase64, 'base64').toString('utf8'); // Verify signature const expectedSignature = this.signPayload(payload); if (!this.constantTimeCompare(signature, expectedSignature)) { return { valid: false, error: 'Invalid token signature' }; } // Parse payload const tokenData = JSON.parse(payload); // Verify session ID if (tokenData.sessionId !== sessionId) { return { valid: false, error: 'Token session mismatch' }; } // Check expiration if (Date.now() > tokenData.expiresAt) { return { valid: false, error: 'Token expired', expired: true }; } return { valid: true }; } catch (error) { return { valid: false, error: 'Token validation failed' }; } } /** * Generate a double-submit cookie token */ generateCookieToken() { const randomData = randomBytes(32); const timestamp = Date.now(); const payload = JSON.stringify({ timestamp, random: randomData.toString('hex') }); const signature = this.signPayload(payload); return Buffer.from(payload).toString('base64') + '.' + signature; } /** * Validate double-submit cookie pattern */ validateDoubleSubmit(cookieToken, headerToken) { if (!cookieToken || !headerToken) { return { valid: false, error: 'Missing CSRF tokens' }; } // For double-submit pattern, tokens should match exactly if (!this.constantTimeCompare(cookieToken, headerToken)) { return { valid: false, error: 'CSRF token mismatch' }; } // Validate the token structure try { const parts = cookieToken.split('.'); if (parts.length !== 2) { return { valid: false, error: 'Invalid token format' }; } const [payloadBase64, signature] = parts; if (!payloadBase64 || !signature) { return { valid: false, error: 'Invalid token format' }; } const payload = Buffer.from(payloadBase64, 'base64').toString('utf8'); // Verify signature const expectedSignature = this.signPayload(payload); if (!this.constantTimeCompare(signature, expectedSignature)) { return { valid: false, error: 'Invalid token signature' }; } return { valid: true }; } catch (error) { return { valid: false, error: 'Token validation failed' }; } } /** * Sign a payload using HMAC-SHA256 */ signPayload(payload) { return createHash('sha256') .update(this.config.secret + payload) .digest('hex'); } /** * Constant-time string comparison to prevent timing attacks */ constantTimeCompare(a, b) { if (a.length !== b.length) { return false; } const bufferA = Buffer.from(a); const bufferB = Buffer.from(b); return timingSafeEqual(bufferA, bufferB); } /** * Get configuration values */ getConfig() { return { ...this.config }; } } //# sourceMappingURL=token-generator.js.map