fix(redis): INCR+EXPIRE atomar via Pipeline (MULTI/EXEC)
Login-, TOTP- und Kiosk-PIN-Lockout zählten Fehlversuche mit separaten INCR/EXPIRE-Calls. Zwischen beiden konnte der Fail-Counter-Key kurzzeitig ohne TTL bestehen bleiben (Crash-Fenster) - nicht atomar. Jetzt per redis.pipeline(transaction=True) als MULTI/EXEC. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Ahyx6D3r7G1EuAc42nezn
This commit is contained in:
@@ -278,8 +278,12 @@ async def _record_totp_failure(user_id: str, redis) -> None:
|
||||
"""Zählt TOTP-Fehlversuch und setzt Lockout nach TOTP_MAX_ATTEMPTS Fehlversuchen."""
|
||||
fail_key = f"totp_fails:{user_id}"
|
||||
lock_key = f"totp_lockout:{user_id}"
|
||||
fails = await redis.incr(fail_key)
|
||||
await redis.expire(fail_key, TOTP_LOCKOUT_SECONDS)
|
||||
# INCR+EXPIRE atomar (MULTI/EXEC) – sonst könnte fail_key zwischen beiden
|
||||
# Calls kurzzeitig ohne TTL bestehen (Crash-Fenster).
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
pipe.incr(fail_key)
|
||||
pipe.expire(fail_key, TOTP_LOCKOUT_SECONDS)
|
||||
fails, _ = await pipe.execute()
|
||||
if fails >= TOTP_MAX_ATTEMPTS:
|
||||
await redis.set(lock_key, "1", ex=TOTP_LOCKOUT_SECONDS)
|
||||
await redis.delete(fail_key)
|
||||
|
||||
@@ -65,8 +65,12 @@ class AuthService:
|
||||
"""Zählt Fehlversuch und setzt Lockout nach FAILED_LOGIN_MAX Fehlversuchen."""
|
||||
fail_key = f"login_fails:{email.lower()}"
|
||||
lockout_key = f"login_lockout:{email.lower()}"
|
||||
fails = await redis.incr(fail_key)
|
||||
await redis.expire(fail_key, FAILED_LOGIN_LOCKOUT_SEC)
|
||||
# INCR+EXPIRE atomar (MULTI/EXEC) – sonst könnte fail_key zwischen beiden
|
||||
# Calls kurzzeitig ohne TTL bestehen (Crash-Fenster).
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
pipe.incr(fail_key)
|
||||
pipe.expire(fail_key, FAILED_LOGIN_LOCKOUT_SEC)
|
||||
fails, _ = await pipe.execute()
|
||||
if fails >= FAILED_LOGIN_MAX:
|
||||
await redis.set(lockout_key, "1", ex=FAILED_LOGIN_LOCKOUT_SEC)
|
||||
await redis.delete(fail_key)
|
||||
|
||||
@@ -54,8 +54,12 @@ class KioskAuthService:
|
||||
fail_key = f"pin_fails:{device_id}:{personnel_number}"
|
||||
lockout_key = f"pin_lockout:{device_id}:{personnel_number}"
|
||||
|
||||
fails = await redis.incr(fail_key)
|
||||
await redis.expire(fail_key, PIN_LOCKOUT_SECONDS)
|
||||
# INCR+EXPIRE atomar (MULTI/EXEC) – sonst könnte fail_key zwischen beiden
|
||||
# Calls kurzzeitig ohne TTL bestehen (Crash-Fenster).
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
pipe.incr(fail_key)
|
||||
pipe.expire(fail_key, PIN_LOCKOUT_SECONDS)
|
||||
fails, _ = await pipe.execute()
|
||||
|
||||
if fails >= PIN_MAX_ATTEMPTS:
|
||||
await redis.set(lockout_key, "1", ex=PIN_LOCKOUT_SECONDS)
|
||||
|
||||
Reference in New Issue
Block a user