70 lines
3.8 KiB
Markdown
70 lines
3.8 KiB
Markdown
---
|
|
plan: 07.2-03
|
|
phase: 07.2-security-jti-claim-redis-access-token-revocation-inserted
|
|
status: complete
|
|
wave: 2
|
|
completed: 2026-06-06
|
|
|
|
key-files:
|
|
created: []
|
|
modified:
|
|
- backend/api/auth.py
|
|
- backend/api/admin.py
|
|
- backend/tests/test_auth_api.py
|
|
- backend/tests/test_admin_api.py
|
|
---
|
|
|
|
# Plan 07.2-03 Summary — Wave 2: user_nbf Write Sites
|
|
|
|
## What Was Built
|
|
|
|
Wired the `user_nbf` Redis writes into all four security-event handlers that trigger
|
|
access-token revocation, completing the full Phase 7.2 revocation pipeline.
|
|
|
|
**Task 1 — `backend/api/auth.py`:**
|
|
- Added `import time` at module level (after `import hashlib`)
|
|
- Added `await request.app.state.redis.set(f"user_nbf:{current_user.id}", int(time.time()), ex=settings.access_token_expire_minutes * 60)` immediately before `await session.commit()` in:
|
|
- `change_password` handler (uses `request.app.state.redis` directly)
|
|
- `enable_totp` handler (uses `redis_client` already in scope)
|
|
- `disable_totp` handler (uses `request.app.state.redis` directly)
|
|
- Promoted all 3 Wave 0 xfail stubs in `test_auth_api.py` to PASSED assertions
|
|
|
|
**Task 2 — `backend/api/admin.py`:**
|
|
- Added `import time` at module level (after `import uuid`)
|
|
- Added `from config import settings` at module level (was previously only locally imported)
|
|
- Added `await request.app.state.redis.set(f"user_nbf:{user.id}", int(time.time()), ex=settings.access_token_expire_minutes * 60)` inside the `if not body.is_active:` block in the deactivation handler, after `await revoke_all_refresh_tokens(session, user.id)` — does NOT execute on activation path
|
|
- Promoted both Wave 0 admin xfail stubs in `test_admin_api.py` to PASSED assertions
|
|
|
|
**Task 3 — Full regression run:**
|
|
- Full backend suite: 387 passed, 1 failed (pre-existing `test_extract_docx` — `ModuleNotFoundError: No module named 'docx'`, unrelated to Phase 7.2), 7 xfailed (pre-existing from prior phases), 6 skipped
|
|
|
|
## Patterns Established
|
|
|
|
- **user_nbf write pattern**: `await redis.set(f"user_nbf:{user.id}", int(time.time()), ex=settings.access_token_expire_minutes * 60)` before `session.commit()`
|
|
- **Conditional deactivation-only write**: write is strictly inside `if not body.is_active:` — reactivation does NOT write user_nbf (invariant verified by `test_activate_user_does_not_write_user_nbf`)
|
|
- **TTL derived from settings**: `settings.access_token_expire_minutes * 60` — stays synchronized if TTL changes in config, not hardcoded 900
|
|
|
|
## Patterns to Avoid
|
|
|
|
- Do NOT write `user_nbf` on activation/reactivation (is_active=True) — would force legitimate users to re-login immediately
|
|
- Do NOT hardcode TTL as 900 — use `settings.access_token_expire_minutes * 60`
|
|
|
|
## What This Enables
|
|
|
|
Full Phase 7.2 revocation pipeline is now complete:
|
|
1. Every access token contains a `jti` UUID claim (Plan 02)
|
|
2. `get_current_user` reads `user_nbf:{user_id}` from Redis and rejects tokens with `iat < nbf` (Plan 02)
|
|
3. All four security events write `user_nbf:{user_id}` to Redis: change_password, enable_totp, disable_totp (this plan), and admin deactivation (this plan)
|
|
|
|
Any token issued before a security event is blocked on the next request — the 15-minute access-token window is closed.
|
|
|
|
## Self-Check: PASSED
|
|
|
|
- All 9 VALIDATION.md behaviors: PASSED
|
|
- `grep -c "user_nbf" backend/api/auth.py` → 3 (change_password, enable_totp, disable_totp)
|
|
- `grep -c "user_nbf" backend/api/admin.py` → 1 (deactivation only)
|
|
- `grep -c "user_nbf" backend/deps/auth.py` → 5 (reader: multiple log/comparison lines)
|
|
- `except HTTPException: raise` at line 81 precedes `except Exception` at line 83 in `deps/auth.py` — Pitfall 1 guard confirmed
|
|
- `grep -E "^import time" backend/api/auth.py backend/api/admin.py` → 2 lines
|
|
- Zero new test failures introduced by Phase 7.2 (pre-existing `test_extract_docx` failure is a missing `python-docx` module, unrelated)
|