3 plans (Wave 0 test scaffolding, Wave 1 jti+NBF check, Wave 2 user_nbf writes), VALIDATION.md, RESEARCH.md with resolved open questions. Checker: 0 blockers, 1 warning resolved (RESEARCH.md open questions marked). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.2-security-jti-claim-redis-access-token-revocation-inserted | 01 | execute | 0 |
|
true |
|
|
|
Purpose: Two things must be in place before any code in services/auth.py,
deps/auth.py, api/auth.py, or api/admin.py is touched:
-
A
FakeRedisinstance attached toapp.state.redison the minimal test app inbackend/tests/test_auth_deps.py. Without this, Wave 1's NBF check inget_current_userwill raiseAttributeError: 'State' object has no attribute 'redis'on every existing auth-dep test (Pitfall 4 from RESEARCH.md). -
Failing/xfailed test stubs that pin every Phase 7.2 behavior listed in
07.2-VALIDATION.md(JTI presence, NBF write on each security event, NBF check accept/reject, fail-open). Wave 1 and Wave 2 implementation then "flips" these from xfail to passing — verifiable test signal.
Output: 4 modified test files. Test suite still green (zero new failures);
new stubs report as XFAIL with strict=False.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-CONTEXT.md @.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-RESEARCH.md @.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-VALIDATION.md @backend/tests/test_auth_deps.py @backend/tests/test_auth_api.py @backend/tests/test_task2_auth_service.py @backend/tests/test_admin_api.pyExisting FakeRedis class — backend/tests/test_auth_api.py lines 47–86:
- class FakeRedis with async get/set/incr/expire/close
- Already used by authed_client fixture (line 102): fake_redis = FakeRedis(); app.state.redis = fake_redis
- Supports
await redis.get(key)returning the stored value (or None), andawait redis.set(key, value, ex=seconds) - For Phase 7.2 NBF check, the .get() return type must be bytes-like (RESEARCH.md L210:
int(nbf_bytes.decode())). The existing FakeRedis stores arbitrary value types; tests settinguser_nbfmust store bytes (e.g.,b"1700000000") for the .decode() call to work.
Existing make_test_app — backend/tests/test_auth_deps.py lines 22–37:
- Builds a minimal FastAPI with /test/me and /test/admin endpoints
- Does NOT currently set app.state.redis
- Used by auth_client fixture (line 40–51)
Existing test_task2_auth_service.py — backend/tests/test_task2_auth_service.py:
- test_create_access_token_jwt_format (line 28): existing positive test for create_access_token
- test_decode_access_token_valid (line 35): decodes and asserts payload contents — TEMPLATE for jti assertion
Existing test_auth_api.py change-password test pattern:
- Uses
authed_clientfixture (line 89) which setsapp.state.redis = FakeRedis() - change_password handler at backend/api/auth.py:455 — already uses
request.cookies.get("refresh_token")and writes audit log
Existing test_admin_api.py deactivation tests:
- test_deactivate_user at line 191 — PATCH /api/admin/users/{id}/status with is_active=False
- admin_client fixture (line 72) does NOT currently set app.state.redis — Wave 2 (Plan 03) write site uses request.app.state.redis, so admin tests need FakeRedis too (Task 4 below)
Phase 7.2 behaviors to stub (from 07.2-VALIDATION.md Per-Task Verification Map):
- jti-claim: create_access_token payload contains "jti" key with UUID-format string value
- test-fakeredis: get_current_user does not raise AttributeError when app.state.redis is FakeRedis
- nbf-write-change-password: POST /api/auth/change-password sets user_nbf:{user_id} in Redis
- nbf-write-enable-totp: POST /api/auth/totp/enable sets user_nbf:{user_id} in Redis
- nbf-write-disable-totp: DELETE /api/auth/totp sets user_nbf:{user_id} in Redis
- nbf-write-deactivation: PATCH /api/admin/users/{id}/status (is_active=False) sets user_nbf:{user_id} in Redis
- nbf-check-reject: token with iat < nbf returns 401 "Session invalidated"
- nbf-check-allow: token with iat > nbf returns 200
- nbf-fail-open: redis.get raises Exception → request still succeeds (200)
Wave 0 stub convention (from STATE.md key decisions: "Wave 0 stubs: single-line body only"):
- Body is ONLY
pytest.xfail("not implemented yet — Phase 7.2 Wave 1/2", strict=False)or test marker@pytest.mark.xfail(strict=False, reason="...")with placeholder assertion - No real assertion logic in Wave 0 — that comes in Wave 1/2 when the same stubs are promoted by replacing the xfail with real assertions
Modify `make_test_app()` (line 22): before returning test_app, set `test_app.state.redis = FakeRedis()`. Keep the existing route definitions unchanged.
Modify `auth_client` fixture (line 40): no signature change; ensure each invocation creates a fresh FakeRedis (because make_test_app() instantiates it). Add a fixture-scope comment noting that Phase 7.2 NBF check reads `request.app.state.redis`.
Append three new test functions after the existing last test:
- `test_get_current_user_rejects_token_when_iat_before_user_nbf(auth_client, db_session)`
- `test_get_current_user_allows_token_when_iat_after_user_nbf(auth_client, db_session)`
- `test_get_current_user_failopen_on_redis_error(auth_client, db_session)`
Each new test:
- decorated with `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — NBF check not yet implemented in get_current_user")`
- decorated with `@pytest.mark.asyncio`
- Uses _create_user helper (already present, line 54) to insert a user
- Uses services.auth.create_access_token to mint the token
- Body: 1–5 lines of placeholder assertions (e.g., `assert False, "stub"` after the prep), so when Wave 1 implementation lands and the assertion logic is filled in, the xfail flips to xpass. Keep Wave 0 body minimal per STATE.md convention.
For Task 4 (failopen), define a small inline `class _BrokenRedis` whose `async def get(self, key)` raises `RuntimeError("simulated redis down")`. Set `auth_client._transport.app.state.redis = _BrokenRedis()` inside the test (or override via a fixture-local app instance) — this is acceptable in Wave 0 because Wave 1 implementation will read the assignment back out.
Do NOT modify any existing test in the file. Do NOT add real assertion logic for the NBF check beyond placeholder stubs.
`def test_create_access_token_includes_jti_claim():` decorated with `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — jti claim not yet added to create_access_token")`.
Body: import create_access_token and decode_access_token from services.auth (follow the import style of the surrounding tests — they use function-local imports per test). Mint a token. Decode it. Assert `"jti" in payload` and `uuid.UUID(payload["jti"])` succeeds. Import `uuid` at function scope (matches surrounding style) if not already module-level imported.
Do NOT modify create_access_token in services/auth.py — that is Wave 1 (Plan 02). Do NOT modify any existing test.
Implementation hint (kept in test comments for Wave 2): after the API call returns success, fetch the user_id from the login response payload, then `nbf_bytes = await authed_client._transport.app.state.redis.get(f"user_nbf:{user_id}")` and assert `nbf_bytes is not None` plus `int(nbf_bytes.decode() if isinstance(nbf_bytes, (bytes, bytearray)) else nbf_bytes) > 0`.
Do NOT modify any handler in backend/api/auth.py — that is Wave 2 (Plan 03). Do NOT modify the FakeRedis class.
Modify `admin_client` fixture (line 72): before `async with AsyncClient(...)`, add `app.state.redis = FakeRedis()`. In the teardown after fixture yield (after `app.dependency_overrides.clear()`), add `app.state.redis = None` (mirrors authed_client teardown at test_auth_api.py:123).
Append two new test functions at end of file (after the last existing test):
- `test_deactivate_user_writes_user_nbf_to_redis(admin_client)` — async, marked `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to admin deactivation handler")`. Body: unpack `client, _admin, session = admin_client`; create a regular user via `make_regular_user(session)`; PATCH /api/admin/users/{user.id}/status with `{"is_active": false}`; assert response 200; access `client._transport.app.state.redis` (the FakeRedis instance) and `await` its `.get(f"user_nbf:{user.id}")`; assert the value is not None. Placeholder body acceptable in Wave 0 (single `assert False, "stub"` line is fine).
- `test_activate_user_does_not_write_user_nbf(admin_client)` — same xfail decorator; deactivate first, then PATCH with `{"is_active": true}`, assert `await app.state.redis.get(f"user_nbf:{user.id}")` is None at the end. Placeholder body acceptable.
Verify no existing admin test regresses: pre-existing admin tests do not call `request.app.state.redis` so the mount is additive and safe.
Do NOT modify any handler in backend/api/admin.py — that is Wave 2 (Plan 03).
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| test → app.state | Test fixtures inject fake infrastructure (FakeRedis) onto the live FastAPI app; tests must not leak shared state between test runs |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-7.2-W0-01 | Tampering | test_admin_api.py admin_client fixture | mitigate | Reset app.state.redis = None in teardown after yield (mirrors authed_client convention at test_auth_api.py:123) — prevents stale FakeRedis from one test file bleeding into another |
| T-7.2-W0-02 | Repudiation | xfail stubs with strict=False |
accept | Stubs are deliberately permissive in Wave 0 — they document expected behavior and act as failing gates when Wave 1/2 lands. The strict=False convention is established across this project (STATE.md). |
| T-7.2-W0-SC | Tampering | npm/pip/cargo installs | n/a | No packages installed in this plan — Phase 7.2 adds zero dependencies (RESEARCH.md "Package Legitimacy Audit" section is intentionally empty) |
| </threat_model> |
<success_criteria>
- Wave 0 stubs cover every behavior in 07.2-VALIDATION.md Per-Task Verification Map (jti-claim, test-fakeredis, nbf-write × 4, nbf-check × 2, nbf-fail-open)
- FakeRedis is reachable via
request.app.state.redisin both test_auth_deps and test_admin_api fixtures - Wave 1 (Plan 02) and Wave 2 (Plan 03) can promote each stub to a passing assertion by editing only the test body — no new fixtures, no new files
- Zero regressions: existing 373-test baseline from Phase 7.1 passes unchanged </success_criteria>
Required fields in SUMMARY: artifacts (4 test files), patterns_established (FakeRedis on test_auth_deps app.state, FakeRedis on admin_client fixture), patterns_to_avoid (do NOT redefine FakeRedis — import from tests.test_auth_api), provides (Wave 0 scaffolds for jti, NBF-check, NBF-write × 4, fail-open).