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>
338 lines
24 KiB
Markdown
338 lines
24 KiB
Markdown
---
|
||
phase: 07.2-security-jti-claim-redis-access-token-revocation-inserted
|
||
plan: 01
|
||
type: execute
|
||
wave: 0
|
||
depends_on: []
|
||
files_modified:
|
||
- backend/tests/test_auth_deps.py
|
||
- backend/tests/test_task2_auth_service.py
|
||
- backend/tests/test_auth_api.py
|
||
- backend/tests/test_admin_api.py
|
||
autonomous: true
|
||
requirements:
|
||
- CONCERNS:JTI-CLAIM
|
||
- CONCERNS:JTI-REVOKE-REDIS
|
||
tags:
|
||
- security
|
||
- jwt
|
||
- testing
|
||
- nyquist
|
||
|
||
must_haves:
|
||
truths:
|
||
- "Every Phase 7.2 behavior has a failing test scaffold before any implementation runs"
|
||
- "test_auth_deps.py test app mounts app.state.redis = FakeRedis() so Wave 1 NBF check does not crash existing 34+ auth-dep tests"
|
||
- "Wave 0 stubs are skipped/xfailed (strict=False); zero existing tests regress"
|
||
artifacts:
|
||
- path: "backend/tests/test_auth_deps.py"
|
||
provides: "FakeRedis attached to make_test_app(); NBF-check xfail stubs (iat<nbf→401, iat>nbf→pass, fail-open)"
|
||
contains: "FakeRedis"
|
||
- path: "backend/tests/test_task2_auth_service.py"
|
||
provides: "test_create_access_token_includes_jti xfail stub"
|
||
contains: "jti"
|
||
- path: "backend/tests/test_auth_api.py"
|
||
provides: "NBF-write xfail stubs for change_password, enable_totp, disable_totp"
|
||
contains: "user_nbf"
|
||
- path: "backend/tests/test_admin_api.py"
|
||
provides: "NBF-write xfail stub for admin deactivation handler"
|
||
contains: "user_nbf"
|
||
key_links:
|
||
- from: "backend/tests/test_auth_deps.py make_test_app()"
|
||
to: "request.app.state.redis"
|
||
via: "test_app.state.redis = FakeRedis() set before client yield"
|
||
pattern: "app\\.state\\.redis\\s*=\\s*FakeRedis"
|
||
- from: "Wave 0 stubs"
|
||
to: "Wave 1/2 implementation"
|
||
via: "pytest.xfail(strict=False) markers promote to xpass when code lands"
|
||
pattern: "pytest\\.xfail|xfail.*strict=False"
|
||
---
|
||
|
||
<objective>
|
||
Pre-implementation scaffolding for Phase 7.2 (Nyquist Wave 0).
|
||
|
||
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:
|
||
|
||
1. A `FakeRedis` instance attached to `app.state.redis` on the minimal test
|
||
app in `backend/tests/test_auth_deps.py`. Without this, Wave 1's NBF
|
||
check in `get_current_user` will raise `AttributeError: 'State' object
|
||
has no attribute 'redis'` on every existing auth-dep test (Pitfall 4
|
||
from RESEARCH.md).
|
||
|
||
2. 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`.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||
</execution_context>
|
||
|
||
<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.py
|
||
|
||
<interfaces>
|
||
<!-- Reusable FakeRedis class already exists. DO NOT redefine. -->
|
||
|
||
Existing 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), and `await 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 setting `user_nbf` must 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_client` fixture (line 89) which sets `app.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
|
||
</interfaces>
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: Mount FakeRedis on test_auth_deps test app and add NBF-check stubs</name>
|
||
<files>backend/tests/test_auth_deps.py</files>
|
||
<read_first>
|
||
- backend/tests/test_auth_deps.py (current make_test_app + auth_client fixture — lines 22–51)
|
||
- backend/tests/test_auth_api.py lines 47–86 (FakeRedis class definition — import this, do not redefine)
|
||
- backend/tests/test_auth_api.py lines 89–123 (authed_client fixture — pattern for app.state.redis assignment)
|
||
- .planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-RESEARCH.md "Pitfall 4" section (lines 296–301)
|
||
</read_first>
|
||
<behavior>
|
||
- Test 1 (positive guard): existing test_get_current_user_returns_user still passes after FakeRedis is attached (no regression).
|
||
- Test 2 (xfail): test_get_current_user_rejects_token_when_iat_before_user_nbf — pre-populates fake_redis["user_nbf:{uid}"] = b"<future_ts>", calls /test/me with a token whose iat < future_ts, expects 401 with detail containing "Session invalidated".
|
||
- Test 3 (xfail): test_get_current_user_allows_token_when_iat_after_user_nbf — pre-populates fake_redis["user_nbf:{uid}"] = b"<past_ts>", calls /test/me with a token whose iat > past_ts, expects 200.
|
||
- Test 4 (xfail): test_get_current_user_failopen_on_redis_error — sets app.state.redis to an object whose get() raises Exception, calls /test/me, expects 200 (fail-open per D-04).
|
||
- All three new tests marked `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 1 — NBF check not yet implemented")`.
|
||
</behavior>
|
||
<action>
|
||
Import FakeRedis from tests.test_auth_api at top of file (use relative-style import: `from tests.test_auth_api import FakeRedis` — verify by reading existing `from ...` statements in other Phase tests for the correct module path; conftest uses `from main import app` so absolute `from tests.test_auth_api` is the project convention).
|
||
|
||
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.
|
||
</action>
|
||
<verify>
|
||
<automated>cd backend && pytest tests/test_auth_deps.py -v</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `cd backend && pytest tests/test_auth_deps.py -v` exits 0
|
||
- `grep -c "FakeRedis" backend/tests/test_auth_deps.py` returns >= 2 (import + assignment)
|
||
- `grep -c "test_app.state.redis" backend/tests/test_auth_deps.py` returns >= 1
|
||
- `grep -c "test_get_current_user_rejects_token_when_iat_before_user_nbf\|test_get_current_user_allows_token_when_iat_after_user_nbf\|test_get_current_user_failopen_on_redis_error" backend/tests/test_auth_deps.py` returns 3
|
||
- `grep -E -c "pytest\\.mark\\.xfail|pytest\\.xfail" backend/tests/test_auth_deps.py` returns >= 3
|
||
- Test output lists at least 3 XFAIL results (or XPASS for any whose body already trivially passes — both acceptable in strict=False)
|
||
- All previously-existing tests in test_auth_deps.py still PASS (no regressions in the 34+ baseline)
|
||
</acceptance_criteria>
|
||
<done>FakeRedis mounted on test app; three NBF-check stubs added with xfail(strict=False); zero existing test regressions.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 2: Add JTI presence stub in test_task2_auth_service.py</name>
|
||
<files>backend/tests/test_task2_auth_service.py</files>
|
||
<read_first>
|
||
- backend/tests/test_task2_auth_service.py lines 28–58 (existing create/decode_access_token tests — TEMPLATE)
|
||
- backend/services/auth.py lines 86–117 (current create_access_token + decode_access_token implementations — note absence of jti)
|
||
</read_first>
|
||
<behavior>
|
||
- Test (xfail): test_create_access_token_includes_jti_claim — calls `create_access_token("test-uid", "user")`, decodes via `decode_access_token`, asserts the returned payload dict has key `"jti"` whose value is a non-empty string parseable as a UUID (uuid.UUID(payload["jti"]) does not raise).
|
||
</behavior>
|
||
<action>
|
||
Append a single new test function after the existing last create/decode_access_token test in the file:
|
||
|
||
`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.
|
||
</action>
|
||
<verify>
|
||
<automated>cd backend && pytest tests/test_task2_auth_service.py -v -k jti</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `cd backend && pytest tests/test_task2_auth_service.py -v -k jti` exits 0
|
||
- `grep -c "test_create_access_token_includes_jti_claim" backend/tests/test_task2_auth_service.py` returns 1
|
||
- `grep -E -c "xfail.*strict=False" backend/tests/test_task2_auth_service.py` returns >= 1 (new stub)
|
||
- The new test reports as XFAIL (or XPASS if the placeholder body trivially passes; both acceptable in strict=False)
|
||
- All existing tests in test_task2_auth_service.py still PASS (zero regressions)
|
||
</acceptance_criteria>
|
||
<done>JTI presence test stub added with xfail(strict=False); existing tests unaffected.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 3: Add NBF-write stubs for change_password / enable_totp / disable_totp in test_auth_api.py</name>
|
||
<files>backend/tests/test_auth_api.py</files>
|
||
<read_first>
|
||
- backend/tests/test_auth_api.py lines 45–123 (FakeRedis class + authed_client fixture — already mounts app.state.redis)
|
||
- backend/api/auth.py lines 452–509 (change_password handler — Wave 2 will add the user_nbf write here)
|
||
- backend/api/auth.py lines 548–601 (enable_totp handler — Wave 2 write site)
|
||
- backend/api/auth.py lines 606–641 (disable_totp handler — Wave 2 write site)
|
||
- Existing test that exercises change_password in test_auth_api.py (grep for `change-password` and copy its login+TOTP setup pattern)
|
||
</read_first>
|
||
<behavior>
|
||
- Test (xfail) 1: test_change_password_writes_user_nbf_to_redis — completes register + login (using existing helpers), then POST /api/auth/change-password with valid current+new passwords. After response, asserts `await app.state.redis.get(f"user_nbf:{user_id}")` returns a non-None bytes value parseable as `int(...)`.
|
||
- Test (xfail) 2: test_enable_totp_writes_user_nbf_to_redis — registers, logs in, calls /totp/setup, supplies a generated TOTP code via /totp/enable, asserts `user_nbf:{user_id}` is set in Redis.
|
||
- Test (xfail) 3: test_disable_totp_writes_user_nbf_to_redis — same setup as Test 2 plus enable, then DELETE /api/auth/totp, asserts `user_nbf:{user_id}` is set in Redis.
|
||
</behavior>
|
||
<action>
|
||
Append three new async test functions (after the existing change_password / TOTP tests) named exactly as listed in behavior above. All three:
|
||
- decorated `@pytest.mark.xfail(strict=False, reason="Phase 7.2 Wave 2 — user_nbf write not yet added to handler")`
|
||
- decorated `@pytest.mark.asyncio`
|
||
- Use the `authed_client` fixture (already mounts FakeRedis at app.state.redis)
|
||
- Reuse the existing `_register` / `_login` helpers (lines 31–43) and the in-file TOTP test helpers (grep for existing TOTP-enable test pattern in the file; copy fixture/imports)
|
||
- Body keeps Wave 0 minimal: prep + a single `assert False, "stub — Wave 2 will fill in"` or call `pytest.xfail("Phase 7.2 Wave 2 stub")` inside the body. The xfail strict=False allows both.
|
||
|
||
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.
|
||
</action>
|
||
<verify>
|
||
<automated>cd backend && pytest tests/test_auth_api.py -v -k "nbf or user_nbf"</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `cd backend && pytest tests/test_auth_api.py -v -k "nbf or user_nbf"` exits 0
|
||
- `grep -c "test_change_password_writes_user_nbf_to_redis\|test_enable_totp_writes_user_nbf_to_redis\|test_disable_totp_writes_user_nbf_to_redis" backend/tests/test_auth_api.py` returns 3
|
||
- `grep -E -c "user_nbf" backend/tests/test_auth_api.py` returns >= 3 (one per new test)
|
||
- `grep -E -c "xfail.*strict=False" backend/tests/test_auth_api.py` returns >= 3 (new stubs)
|
||
- All three new tests report as XFAIL (or XPASS — both acceptable in strict=False)
|
||
- All existing tests in test_auth_api.py still PASS (zero regressions to register/login/totp/change-password baseline)
|
||
</acceptance_criteria>
|
||
<done>Three NBF-write stubs added covering change_password, enable_totp, disable_totp; existing auth API tests unaffected.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 4: Add NBF-write stub + FakeRedis for admin deactivation in test_admin_api.py</name>
|
||
<files>backend/tests/test_admin_api.py</files>
|
||
<read_first>
|
||
- backend/tests/test_admin_api.py lines 71–84 (admin_client fixture — does NOT currently set app.state.redis)
|
||
- backend/tests/test_admin_api.py lines 191–220 (test_deactivate_user — TEMPLATE for the new test)
|
||
- backend/tests/test_auth_api.py lines 47–86 (FakeRedis class — import target)
|
||
- backend/api/admin.py lines 340–380 (deactivation handler — Wave 2 write site)
|
||
</read_first>
|
||
<behavior>
|
||
- admin_client fixture mounts `app.state.redis = FakeRedis()` so the deactivation handler (post-Wave 2) can call `request.app.state.redis.set(...)` without AttributeError.
|
||
- Test (xfail): test_deactivate_user_writes_user_nbf_to_redis — creates a regular user via make_regular_user, sends PATCH /api/admin/users/{id}/status with `{"is_active": false}`, asserts `await app.state.redis.get(f"user_nbf:{user_id}")` returns a non-None bytes value.
|
||
- Test (negative guard, can be passing immediately): test_activate_user_does_NOT_write_user_nbf — sends PATCH with `{"is_active": true}` on an already-deactivated user, asserts Redis key is NOT set. Marked `@pytest.mark.xfail(strict=False)` because the handler does not yet write the key in either branch — Wave 2 must preserve this invariant. (Mirrors RESEARCH.md Anti-pattern: "Do not write user_nbf for successful activation".)
|
||
</behavior>
|
||
<action>
|
||
Import FakeRedis at top of file: `from tests.test_auth_api import FakeRedis` (matches project convention for cross-test imports).
|
||
|
||
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).
|
||
</action>
|
||
<verify>
|
||
<automated>cd backend && pytest tests/test_admin_api.py -v</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `cd backend && pytest tests/test_admin_api.py -v` exits 0
|
||
- `grep -c "from tests.test_auth_api import FakeRedis\|from tests\\.test_auth_api import FakeRedis" backend/tests/test_admin_api.py` returns >= 1
|
||
- `grep -c "app.state.redis = FakeRedis" backend/tests/test_admin_api.py` returns >= 1
|
||
- `grep -c "test_deactivate_user_writes_user_nbf_to_redis\|test_activate_user_does_not_write_user_nbf" backend/tests/test_admin_api.py` returns 2
|
||
- `grep -E -c "xfail.*strict=False" backend/tests/test_admin_api.py` returns >= 2
|
||
- All previously-existing admin tests (including test_deactivate_user, test_reactivate_user) still PASS
|
||
- Full pytest -v reports zero NEW failures vs baseline (XFAILs and XPASSes acceptable)
|
||
</acceptance_criteria>
|
||
<done>FakeRedis attached to admin_client fixture; deactivation NBF-write stubs (positive + negative-guard) added; existing admin tests unaffected.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<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>
|
||
|
||
<verification>
|
||
- `cd backend && pytest tests/test_auth_deps.py tests/test_auth_api.py tests/test_task2_auth_service.py tests/test_admin_api.py -v` — zero new failures vs current baseline (373 passed in Phase 7.1)
|
||
- `grep -c "FakeRedis" backend/tests/test_auth_deps.py` returns >= 2 (import + assignment)
|
||
- `grep -c "FakeRedis" backend/tests/test_admin_api.py` returns >= 2 (import + assignment)
|
||
- All four new stub test sets report as XFAIL (or XPASS — both acceptable under strict=False)
|
||
</verification>
|
||
|
||
<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.redis` in 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>
|
||
|
||
<output>
|
||
Create `.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-01-SUMMARY.md` when done.
|
||
|
||
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).
|
||
</output>
|