diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index b9ec90a..f207f62 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -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) diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 1e1695d..64ab4a7 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -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) diff --git a/backend/app/services/kiosk_auth_service.py b/backend/app/services/kiosk_auth_service.py index 2dc9f9b..ec00e49 100644 --- a/backend/app/services/kiosk_auth_service.py +++ b/backend/app/services/kiosk_auth_service.py @@ -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)