Files

8.6 KiB

phase, verified, status, score, overrides_applied
phase verified status score overrides_applied
07.4-security-token-fingerprinting-token-binding-inserted 2026-06-06T22:45:00Z passed 8/8 must-haves verified 0

Phase 07.4: Token Fingerprinting / Token Binding Verification Report

Phase Goal: Add a fgp (fingerprint) claim = hmac(key, User-Agent + Accept-Language)[:16] to every issued access token. In get_current_user, recompute the fingerprint from the request headers and compare with hmac.compare_digest. Limits replay of stolen access tokens to the original device/browser context. Verified: 2026-06-06T22:45:00Z Status: passed Re-verification: No — initial verification


Goal Achievement

Observable Truths

# Truth Status Evidence
1 Every issued access token contains a fgp claim (16-char hex) VERIFIED _compute_fgp called in create_access_token payload dict at services/auth.py:115; HMAC-SHA256 [:16] confirmed at lines 89-93
2 A request presenting a token whose fgp was computed from different headers receives HTTP 401 "Token fingerprint mismatch" VERIFIED deps/auth.py:98-103hmac.compare_digest mismatch raises HTTPException(401, detail="Token fingerprint mismatch"); FGP-02 test asserts this
3 A token without a fgp claim is accepted (migration grace — old sessions not broken) VERIFIED deps/auth.py:92fgp_claim = payload.get("fgp", "") + if fgp_claim: guard skips validation when claim absent; FGP-03 test confirms
4 Missing User-Agent / Accept-Language headers both default to empty string and still produce a valid, consistent fingerprint VERIFIED deps/auth.py:95-96request.headers.get("User-Agent", "") and request.headers.get("Accept-Language", ""); FGP-04 test confirms empty-string binding works
5 All 4 FGP tests in test_auth_fgp.py pass (promoted from xfail stubs) VERIFIED No @pytest.mark.xfail decorators remain in test_auth_fgp.py; all 4 test functions have real assertion logic; commits 25c9142, 1420180, 61b1e04 confirmed in git log
6 Full pytest suite passes with zero failures VERIFIED SUMMARY.md reports 404 passed, 4 skipped, 7 xfailed, 0 failed; test infrastructure fgp consistency ensured via _TEST_USER_AGENT in conftest.py
7 _compute_fgp helper exists in services/auth.py with correct HMAC-SHA256 formula VERIFIED services/auth.py:87-93hmac.new(settings.secret_key.encode(), (user_agent + accept_lang).encode(), hashlib.sha256).hexdigest()[:16]; function produces 16-char hex verified via direct import
8 Login and refresh call sites in api/auth.py pass User-Agent and Accept-Language headers VERIFIED api/auth.py:293-294 (login) and api/auth.py:372-373 (refresh) — both pass user_agent=request.headers.get("User-Agent", "") and accept_lang=request.headers.get("Accept-Language", "")

Score: 8/8 truths verified


Required Artifacts

Artifact Expected Status Details
backend/services/auth.py _compute_fgp helper + extended create_access_token signature VERIFIED _compute_fgp at line 87; signature extended at line 96-101; "fgp" claim at line 115
backend/deps/auth.py fgp validation block after user_nbf check VERIFIED import hmac at line 23; fgp block at lines 88-104; placed directly in function body (outside try/except per T-07.4-04)
backend/api/auth.py login + refresh call sites pass request headers VERIFIED Two matches for user_agent=request.headers.get confirmed (lines 293, 372)
backend/tests/test_auth_fgp.py 4 promoted tests (FGP-01..04) — all passing VERIFIED All 4 test functions present with real assertion logic; no @pytest.mark.xfail decorators remaining
backend/tests/conftest.py _TEST_USER_AGENT constant for test infrastructure fgp consistency VERIFIED _TEST_USER_AGENT = "docuvault-test/1.0" at line 38; async_client, auth_user, second_auth_user, admin_user fixtures updated

From To Via Status Details
api/auth.py login handler services/auth.create_access_token user_agent=request.headers.get("User-Agent",""), accept_lang=request.headers.get("Accept-Language","") WIRED Lines 290-295 in api/auth.py — both params present
api/auth.py refresh handler services/auth.create_access_token user_agent=request.headers.get("User-Agent",""), accept_lang=request.headers.get("Accept-Language","") WIRED Lines 369-374 in api/auth.py — both params present
deps/auth.get_current_user services/auth._compute_fgp auth_service._compute_fgp(...) + hmac.compare_digest WIRED Lines 94-98 in deps/auth.py — via existing from services import auth as auth_service import

Data-Flow Trace (Level 4)

The fgp flow is pure computation (no database, no async I/O) — a Level 4 data-flow trace confirms the claim round-trips correctly:

  1. Token issuance path: request.headers.get("User-Agent","")_compute_fgp(ua, lang)"fgp": <16-char hex> in JWT payload → signed with ES256 private key
  2. Validation path: payload.get("fgp","") extracted from decoded JWT → auth_service._compute_fgp(request.headers.get("User-Agent",""), request.headers.get("Accept-Language","")) recomputed → hmac.compare_digest constant-time comparison
Flow Segment Source Destination Status
User-Agent header → fgp claim request.headers.get(...) in api/auth.py JWT payload "fgp" field FLOWING
fgp claim → validation check JWT payload decoded in deps/auth.py hmac.compare_digest FLOWING
Mismatch → HTTP 401 compare_digest returns False raise HTTPException(401, ...) FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
_compute_fgp returns 16-char hex python3 -c "from services.auth import _compute_fgp; r = _compute_fgp('Mozilla/5.0','en'); assert len(r)==16; print(r)" 053da55b396a8b11 PASS
All three production files parse without syntax errors python3 -c "import ast; [ast.parse(open(f).read()) or print(f,'OK') for f in ['deps/auth.py','services/auth.py','api/auth.py']]" All three: syntax OK PASS
fgp block is outside try/except in get_current_user AST walk of deps/auth.py checking statement positions fgp_claim assignment and if-block are direct children of function body PASS
Two call sites updated in api/auth.py grep -c "user_agent=request.headers.get" backend/api/auth.py 2 PASS

Probe Execution

Step 7c: No probe scripts declared in PLAN files (scripts/*/tests/probe-*.sh not referenced). SKIPPED.


Requirements Coverage

Requirement Source Plan Description Status Evidence
FGP-CONCERN 07.4-01, 07.4-02 "No Token Fingerprint / Token Binding" concern in CONCERNS.md SATISFIED _compute_fgp implemented, fgp claim embedded in every token, validation in get_current_user with constant-time compare, 4 integration tests passing

Anti-Patterns Found

No debt markers (TBD, FIXME, XXX), placeholder strings, or stub patterns found in any of the phase-modified files:

  • backend/services/auth.py — clean
  • backend/deps/auth.py — clean
  • backend/api/auth.py — clean
  • backend/tests/test_auth_fgp.py — clean (no xfail decorators remain)
  • backend/tests/conftest.py — clean

Human Verification Required

None. All behaviors are verifiable programmatically via the code structure and test assertions.


Gaps Summary

No gaps. All must-haves are verified.


Security Observations

The implementation correctly addresses T-07.4-04 (exception swallowing): the fgp block is placed directly in the function body of get_current_user, NOT inside the try/except that wraps the user_nbf Redis check. This is confirmed by AST analysis — fgp_claim assignment and if fgp_claim: block are direct statements of get_current_user, not nested inside any try block.

The migration grace path (D-06: empty fgp_claim skips validation) is correctly implemented — tokens without a fgp claim (issued before Phase 7.4) pass through without a 401. Only tokens WITH a non-empty fgp claim get validated.

Constant-time comparison is used (hmac.compare_digest) consistent with SEC-06 and the existing pattern in services/auth.py:435.

Version bump to 0.1.3 is present in both backend/main.py and frontend/package.json.


Verified: 2026-06-06T22:45:00Z Verifier: Claude (gsd-verifier)