Files
kite/.planning/phases/07.2-security-jti-claim-redis-access-token-revocation-inserted/07.2-01-PLAN.md
T
curo1305andClaude Sonnet 4.6 7ee87c001b docs(07.2): create phase plan — JTI claim + Redis NBF access-token revocation
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>
2026-06-05 18:56:49 +02:00

338 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 4786:
- 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 2237:
- Builds a minimal FastAPI with /test/me and /test/admin endpoints
- Does NOT currently set app.state.redis
- Used by auth_client fixture (line 4051)
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 2251)
- backend/tests/test_auth_api.py lines 4786 (FakeRedis class definition — import this, do not redefine)
- backend/tests/test_auth_api.py lines 89123 (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 296301)
</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: 15 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 &amp;&amp; pytest tests/test_auth_deps.py -v</automated>
</verify>
<acceptance_criteria>
- `cd backend &amp;&amp; pytest tests/test_auth_deps.py -v` exits 0
- `grep -c "FakeRedis" backend/tests/test_auth_deps.py` returns &gt;= 2 (import + assignment)
- `grep -c "test_app.state.redis" backend/tests/test_auth_deps.py` returns &gt;= 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 &gt;= 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 2858 (existing create/decode_access_token tests — TEMPLATE)
- backend/services/auth.py lines 86117 (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 &amp;&amp; pytest tests/test_task2_auth_service.py -v -k jti</automated>
</verify>
<acceptance_criteria>
- `cd backend &amp;&amp; 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 &gt;= 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 45123 (FakeRedis class + authed_client fixture — already mounts app.state.redis)
- backend/api/auth.py lines 452509 (change_password handler — Wave 2 will add the user_nbf write here)
- backend/api/auth.py lines 548601 (enable_totp handler — Wave 2 write site)
- backend/api/auth.py lines 606641 (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 3143) 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 &amp;&amp; pytest tests/test_auth_api.py -v -k "nbf or user_nbf"</automated>
</verify>
<acceptance_criteria>
- `cd backend &amp;&amp; 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 &gt;= 3 (one per new test)
- `grep -E -c "xfail.*strict=False" backend/tests/test_auth_api.py` returns &gt;= 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 7184 (admin_client fixture — does NOT currently set app.state.redis)
- backend/tests/test_admin_api.py lines 191220 (test_deactivate_user — TEMPLATE for the new test)
- backend/tests/test_auth_api.py lines 4786 (FakeRedis class — import target)
- backend/api/admin.py lines 340380 (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 &amp;&amp; pytest tests/test_admin_api.py -v</automated>
</verify>
<acceptance_criteria>
- `cd backend &amp;&amp; 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 &gt;= 1
- `grep -c "app.state.redis = FakeRedis" backend/tests/test_admin_api.py` returns &gt;= 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 &gt;= 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>