MBO-Tech-IT-Webseite/modules/02-admin-auth/files/lib/rate-limit.ts

108 lines
2.9 KiB
TypeScript

/**
* In-Memory Rate-Limiting für Brute-Force-Schutz
* Speichert Versuche pro Identifier (IP:Email Kombination)
*/
interface RateLimitEntry {
count: number;
lastAttempt: number;
locked: boolean;
lockedUntil?: number;
}
const attempts = new Map<string, RateLimitEntry>();
// Cleanup: Alte Einträge alle 10 Minuten löschen (Memory-Leak vermeiden)
const CLEANUP_INTERVAL = 10 * 60 * 1000;
const RESET_WINDOW = 15 * 60 * 1000; // 15 Minuten Fenster
const MAX_ATTEMPTS = 5; // Max 5 Versuche pro Fenster
const LOCK_THRESHOLD = 10; // Account sperren nach 10 Versuchen
const LOCK_DURATION = 15 * 60 * 1000; // 15 Minuten Sperrung
setInterval(() => {
const now = Date.now();
for (const [key, entry] of attempts.entries()) {
if (now - entry.lastAttempt > RESET_WINDOW) {
attempts.delete(key);
}
}
}, CLEANUP_INTERVAL);
/**
* Prüfe ob Request innerhalb von Rate-Limit liegt
* @param identifier - Eindeutige ID (z.B. "login:admin@example.com:192.168.1.1")
* @returns { allowed, delayMs, locked }
*/
export function checkRateLimit(identifier: string) {
const now = Date.now();
let entry = attempts.get(identifier);
// Neuer Eintrag
if (!entry) {
attempts.set(identifier, {
count: 1,
lastAttempt: now,
locked: false,
});
return { allowed: true, delayMs: 0, locked: false };
}
// Fenster abgelaufen? → Zurücksetzen
if (now - entry.lastAttempt > RESET_WINDOW) {
attempts.set(identifier, {
count: 1,
lastAttempt: now,
locked: false,
});
return { allowed: true, delayMs: 0, locked: false };
}
// Account gesperrt?
if (entry.locked && entry.lockedUntil) {
if (now < entry.lockedUntil) {
const remainingMs = entry.lockedUntil - now;
return { allowed: false, delayMs: remainingMs, locked: true };
} else {
// Sperrung abgelaufen
entry.locked = false;
entry.count = 1;
entry.lastAttempt = now;
return { allowed: true, delayMs: 0, locked: false };
}
}
// Versuch inkrementieren
entry.count++;
entry.lastAttempt = now;
// Exponential Backoff: 1s, 2s, 4s, 8s, 16s, 30s max
const delayMs = Math.min(Math.pow(2, entry.count - 2) * 1000, 30000);
// Nach 10 Versuchen sperren
if (entry.count >= LOCK_THRESHOLD) {
entry.locked = true;
entry.lockedUntil = now + LOCK_DURATION;
return { allowed: false, delayMs: LOCK_DURATION, locked: true };
}
// Vor dem 5. Versuch noch erlaubt, danach mit Delay
const allowed = entry.count <= MAX_ATTEMPTS;
return { allowed, delayMs: allowed ? 0 : delayMs, locked: false };
}
/**
* Manuelles Zurücksetzen (z.B. nach erfolgreichem Login)
*/
export function resetRateLimit(identifier: string) {
attempts.delete(identifier);
}
/**
* Hole Versuchszahl für einen Identifier (für Monitoring)
*/
export function getAttemptCount(identifier: string): number {
const entry = attempts.get(identifier);
return entry?.count ?? 0;
}