Files
Todo/lib/rate-limit.ts
Paul Bailey 534796edd7 feat: implement security enhancements (task 17)
- Add rate limiting for login, registration, and invitations
- Implement CSRF protection with SameSite cookies and security headers
- Configure secure session cookies with httpOnly and secure flags
- Enhance input sanitization for XSS and injection prevention
- Add comprehensive security tests (41 tests passing)
- Document security enhancements in SECURITY_ENHANCEMENTS.md

Requirements: 9.3, 9.4, 9.5
2025-12-02 01:29:35 +00:00

190 lines
4.6 KiB
TypeScript

/**
* Rate limiting utilities for security
* Implements in-memory rate limiting for login attempts and other sensitive operations
*
* Requirements: 9.3 - Rate limiting on authentication requests
*/
interface RateLimitEntry {
count: number
resetAt: number
}
// In-memory store for rate limiting
// In production, consider using Redis or similar for distributed systems
const rateLimitStore = new Map<string, RateLimitEntry>()
// Cleanup interval to prevent memory leaks
const CLEANUP_INTERVAL = 60 * 60 * 1000 // 1 hour
setInterval(() => {
const now = Date.now()
const entries = Array.from(rateLimitStore.entries())
for (const [key, entry] of entries) {
if (entry.resetAt < now) {
rateLimitStore.delete(key)
}
}
}, CLEANUP_INTERVAL)
export interface RateLimitConfig {
/**
* Maximum number of requests allowed within the window
*/
maxRequests: number
/**
* Time window in milliseconds
*/
windowMs: number
/**
* Optional identifier (e.g., IP address, user ID)
* If not provided, uses a default key
*/
identifier?: string
}
export interface RateLimitResult {
/**
* Whether the request is allowed
*/
allowed: boolean
/**
* Number of requests remaining in the current window
*/
remaining: number
/**
* Timestamp when the rate limit will reset
*/
resetAt: number
/**
* Time in milliseconds until the rate limit resets
*/
retryAfter?: number
}
/**
* Check if a request is allowed based on rate limiting rules
*
* @param key - Unique identifier for the rate limit (e.g., "login:user@example.com" or "login:192.168.1.1")
* @param config - Rate limit configuration
* @returns Rate limit result indicating if request is allowed
*/
export function checkRateLimit(
key: string,
config: RateLimitConfig
): RateLimitResult {
const now = Date.now()
const entry = rateLimitStore.get(key)
// If no entry exists or the window has expired, create a new entry
if (!entry || entry.resetAt < now) {
const resetAt = now + config.windowMs
rateLimitStore.set(key, {
count: 1,
resetAt,
})
return {
allowed: true,
remaining: config.maxRequests - 1,
resetAt,
}
}
// Check if limit has been exceeded
if (entry.count >= config.maxRequests) {
return {
allowed: false,
remaining: 0,
resetAt: entry.resetAt,
retryAfter: entry.resetAt - now,
}
}
// Increment count and allow request
entry.count++
rateLimitStore.set(key, entry)
return {
allowed: true,
remaining: config.maxRequests - entry.count,
resetAt: entry.resetAt,
}
}
/**
* Reset rate limit for a specific key
* Useful for clearing rate limits after successful authentication
*
* @param key - Unique identifier for the rate limit
*/
export function resetRateLimit(key: string): void {
rateLimitStore.delete(key)
}
/**
* Rate limit configuration for login attempts
* Allows 5 login attempts per 15 minutes per email address
*/
export const LOGIN_RATE_LIMIT: Omit<RateLimitConfig, 'identifier'> = {
maxRequests: 5,
windowMs: 15 * 60 * 1000, // 15 minutes
}
/**
* Rate limit configuration for registration attempts
* Allows 3 registration attempts per hour per IP address
*/
export const REGISTER_RATE_LIMIT: Omit<RateLimitConfig, 'identifier'> = {
maxRequests: 3,
windowMs: 60 * 60 * 1000, // 1 hour
}
/**
* Rate limit configuration for password reset attempts
* Allows 3 password reset attempts per hour per email
*/
export const PASSWORD_RESET_RATE_LIMIT: Omit<RateLimitConfig, 'identifier'> = {
maxRequests: 3,
windowMs: 60 * 60 * 1000, // 1 hour
}
/**
* Rate limit configuration for invitation creation
* Allows 10 invitations per hour per user
*/
export const INVITATION_RATE_LIMIT: Omit<RateLimitConfig, 'identifier'> = {
maxRequests: 10,
windowMs: 60 * 60 * 1000, // 1 hour
}
/**
* Get client identifier from request headers
* Uses X-Forwarded-For header if available, otherwise falls back to a default
*
* @param headers - Request headers
* @returns Client identifier (IP address or default)
*/
export function getClientIdentifier(headers: Headers): string {
// Try to get IP from X-Forwarded-For header (for proxied requests)
const forwardedFor = headers.get('x-forwarded-for')
if (forwardedFor) {
// X-Forwarded-For can contain multiple IPs, use the first one
return forwardedFor.split(',')[0].trim()
}
// Try to get IP from X-Real-IP header
const realIp = headers.get('x-real-ip')
if (realIp) {
return realIp
}
// Fallback to a default identifier
// In production, you might want to handle this differently
return 'unknown'
}