tasq/node_modules/@claude-flow/security/dist/token-generator.js

351 lines
10 KiB
JavaScript

/**
* 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