Gap (password_reset_confirm missing user_nbf write) was fixed during UAT phase: request: Request param added + Redis write at auth.py:744-750. VALIDATION.md: 84/84 tests, nyquist_compliant=true. VERIFICATION.md updated from gaps_found → passed (9/9). STATE.md advanced to phase 07.3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.0 KiB
phase, verified, status, score, overrides_applied, gaps, deferred, human_verification
| phase | verified | status | score | overrides_applied | gaps | deferred | human_verification | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.2-security-jti-claim-redis-access-token-revocation-inserted | 2026-06-06T00:00:00Z | passed | 9/9 must-haves verified | 0 |
|
Phase 07.2: JTI Claim + Redis NBF Revocation — Verification Report
Phase Goal: JTI claim added to access tokens + Redis-based user_nbf revocation check closes the 15-minute access-token window after refresh-token revocation events Verified: 2026-06-06 Status: passed (9/9) — initial gap (CR-02 password_reset_confirm) resolved during UAT/VALIDATION phase Re-verification: 2026-06-06 — gap confirmed resolved in code + VALIDATION.md (84/84 tests passing)
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | Every access token issued by create_access_token contains a unique jti UUID claim |
VERIFIED | services/auth.py:98 — "jti": str(uuid.uuid4()) present in payload dict after exp; import uuid already at module top (line 25) |
| 2 | get_current_user rejects any access token whose iat predates user_nbf:{user_id} in Redis with HTTP 401 |
VERIFIED | deps/auth.py:68-85 — full NBF check block present; reads user_nbf:{payload['sub']}; raises HTTPException(401, "Session invalidated") when payload["iat"] < int(nbf_str) |
| 3 | get_current_user accepts tokens whose iat post-dates user_nbf (intended recovery path) |
VERIFIED | Same block at deps/auth.py:75 — comparison is strict < not <=; tokens issued after the event pass through |
| 4 | A Redis outage during the NBF check does not block requests — fail-open with warning log | VERIFIED | deps/auth.py:83-84 — broad except Exception as exc: _logger.warning("Redis user_nbf check failed (fail-open): %s", exc) with no re-raise |
| 5 | The HTTPException raised by the NBF check is NOT swallowed by the fail-open broad-catch (Pitfall 1 guard) | VERIFIED | deps/auth.py:81-82 — except HTTPException: raise at line 81 precedes except Exception at line 83; ordering confirmed by grep line numbers |
| 6 | change_password, enable_totp, disable_totp each write user_nbf:{user_id} to Redis before session.commit() |
VERIFIED | api/auth.py:508-511 (change_password), api/auth.py:607-611 (enable_totp via redis_client), api/auth.py:653-657 (disable_totp); all use settings.access_token_expire_minutes * 60 TTL |
| 7 | Admin deactivation writes user_nbf:{user_id} only when is_active=False |
VERIFIED | api/admin.py:353-361 — write is strictly inside if not body.is_active: block; activation path at line 366 has no write |
| 8 | All TTLs use settings.access_token_expire_minutes * 60 (not hardcoded 900) |
VERIFIED | All 4 write sites confirmed by grep: api/auth.py:511,610,656 and api/admin.py:360 — all use the derived expression |
| 9 | password_reset_confirm writes user_nbf to close pre-reset access-token window |
VERIFIED | api/auth.py:701-753 — request: Request parameter added (line 702); user_nbf:{user.id} written at lines 744-750 before session.commit(); covered by test_password_reset_confirm_writes_user_nbf_to_redis (green) |
Score: 9/9 truths verified
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
backend/services/auth.py |
create_access_token with jti=str(uuid.uuid4()) |
VERIFIED | Line 98: "jti": str(uuid.uuid4()), |
backend/deps/auth.py |
user_nbf Redis check with fail-open + HTTPException re-raise guard |
VERIFIED | Lines 62-85: full NBF check block; import logging at line 23; _logger at line 34 |
backend/api/auth.py |
user_nbf write in change_password, enable_totp, disable_totp |
VERIFIED | Lines 507-512, 606-611, 652-657 |
backend/api/admin.py |
user_nbf write in deactivation handler only |
VERIFIED | Lines 356-361; import time at line 26; from config import settings at line 31 |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
services/auth.py create_access_token |
PyJWT encode payload | "jti": str(uuid.uuid4()) in payload dict |
WIRED | Line 98; uuid imported at module level line 25 |
deps/auth.py get_current_user |
request.app.state.redis |
await redis_client.get(f"user_nbf:{payload['sub']}") + int comparison |
WIRED | Lines 69-75 |
api/auth.py change_password |
request.app.state.redis |
await request.app.state.redis.set(f"user_nbf:{current_user.id}", ...) |
WIRED | Line 508-512 |
api/auth.py enable_totp |
request.app.state.redis |
await redis_client.set(f"user_nbf:{current_user.id}", ...) (redis_client in scope) |
WIRED | Lines 607-611 |
api/auth.py disable_totp |
request.app.state.redis |
await request.app.state.redis.set(f"user_nbf:{current_user.id}", ...) |
WIRED | Lines 653-657 |
api/admin.py deactivation handler |
request.app.state.redis |
await request.app.state.redis.set(f"user_nbf:{user.id}", ...) inside if not body.is_active: |
WIRED | Lines 357-361 |
api/auth.py password_reset_confirm |
request.app.state.redis |
user_nbf write | WIRED | request: Request added (line 702); await request.app.state.redis.set(f"user_nbf:{user.id}", ...) at lines 744-750; resolved via UAT/VALIDATION |
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
backend/api/auth.py |
700-753 | user_nbf write after password reset confirm |
Fixed during UAT phase: request: Request added + Redis write at lines 744-750; test green |
|
backend/services/auth.py |
100, 110 | algorithm="HS256" (symmetric) instead of ES256 (asymmetric) |
WARNING | CLAUDE.md Security Protocol mandates ES256; this is tracked and addressed in Phase 7.3 |
Note on HS256 (CR-01 from code review): Phase 7.3 (07.3-security-es256-algorithm-upgrade-inserted) is already planned and has research/plan artifacts present in .planning/phases/. The HS256 finding is deferred to Phase 7.3. It is a WARNING here, not a BLOCKER for Phase 7.2's stated goal — but it is noted.
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| CONCERNS:JTI-CLAIM | 07.2-01, 07.2-02 | Every access token carries a jti UUID claim |
SATISFIED | services/auth.py:98 |
| CONCERNS:JTI-REVOKE-REDIS | 07.2-01, 07.2-02, 07.2-03 | user_nbf Redis check closes 15-min revocation window on security events |
SATISFIED | All 5 write sites verified: change_password (511), enable_totp (610), disable_totp (656), admin deactivation (360), password_reset_confirm (744-750) |
Human Verification Required
(No human verification required — all 9 truths verified by automated tests. 84/84 tests passing per VALIDATION.md 2026-06-06.)
Gaps Summary
No open gaps. The single gap identified in the initial verification (CR-02: password_reset_confirm missing user_nbf write) was resolved during the UAT/VALIDATION phase:
request: Requestparameter added to function signature (line 702)await request.app.state.redis.set(f"user_nbf:{user.id}", int(time.time()), ex=settings.access_token_expire_minutes * 60)written at lines 744-750 beforesession.commit()- Test
test_password_reset_confirm_writes_user_nbf_to_redisintests/test_auth_api.pyis green - VALIDATION.md (2026-06-06): 84/84 tests passing,
nyquist_compliant: true
Verified: 2026-06-06 Verifier: Claude (gsd-verifier)