diff --git a/.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-03-SUMMARY.md b/.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-03-SUMMARY.md new file mode 100644 index 0000000..00c4954 --- /dev/null +++ b/.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-03-SUMMARY.md @@ -0,0 +1,69 @@ +--- +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)