@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
153 lines • 5.45 kB
JavaScript
/**
* 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