security(07.2-02): add user_nbf Redis NBF check to get_current_user
- Added import logging + _logger = logging.getLogger(__name__) to deps/auth.py
- Inserted user_nbf Redis check block after decode_access_token in get_current_user
- Check reads user_nbf:{payload['sub']} from Redis; rejects if payload['iat'] < stored timestamp
- Uses strict < comparison (token issued exactly at event second is allowed — D-02)
- Handles both bytes and str returns from Redis for FakeRedis/aioredis compatibility
- except HTTPException: raise guard precedes broad except Exception (T-7.2-02 Pitfall 1)
- Broad except logs warning and fails open (D-04 — Redis outage must not block requests)
- Promoted all 3 Wave 0 NBF xfail stubs to passing assertions in test_auth_deps.py
This commit is contained in:
@@ -20,6 +20,7 @@ Usage in route handlers:
|
||||
):
|
||||
...
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
@@ -30,6 +31,8 @@ from db.models import User
|
||||
from deps.db import get_db
|
||||
from services import auth as auth_service
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# HTTPBearer parses the Authorization: Bearer <token> header.
|
||||
# auto_error=True (default) raises 403 if no Authorization header is present.
|
||||
security = HTTPBearer()
|
||||
@@ -56,6 +59,31 @@ async def get_current_user(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
) from exc
|
||||
|
||||
# ── user_nbf check (D-02, D-03, D-04) ──────────────────────────────────────
|
||||
# Reject tokens issued before a security event (password change, TOTP enroll,
|
||||
# deactivation). The user_nbf:{user_id} key is written by Wave 2 (Plan 03).
|
||||
# Fail-open on Redis errors (D-04): a Redis outage must never deny service.
|
||||
# CRITICAL: the `except HTTPException: raise` guard MUST precede the broad
|
||||
# `except Exception` — otherwise the intentional 401 is swallowed (T-7.2-02).
|
||||
try:
|
||||
redis_client = request.app.state.redis
|
||||
nbf_bytes = await redis_client.get(f"user_nbf:{payload['sub']}")
|
||||
if nbf_bytes is not None:
|
||||
# Tolerate both bytes and str returns (FakeRedis in tests may return
|
||||
# either depending on what was stored; real aioredis returns bytes).
|
||||
nbf_str = nbf_bytes.decode() if isinstance(nbf_bytes, (bytes, bytearray)) else nbf_bytes
|
||||
if payload["iat"] < int(nbf_str):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Session invalidated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except HTTPException:
|
||||
raise # re-raise the 401 we just constructed (T-7.2-02: Pitfall 1 guard)
|
||||
except Exception as exc:
|
||||
_logger.warning("Redis user_nbf check failed (fail-open): %s", exc)
|
||||
# ── end user_nbf check ──────────────────────────────────────────────────────
|
||||
|
||||
try:
|
||||
user_uuid = uuid.UUID(payload["sub"])
|
||||
except (KeyError, ValueError) as exc:
|
||||
|
||||
Reference in New Issue
Block a user