8.9 KiB
8.9 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.4-security-token-fingerprinting-token-binding-inserted | 02 | auth |
|
|
|
|
|
|
|
35min | 2026-06-06 |
Phase 07.4 Plan 02: Token Fingerprinting (FGP) Implementation Summary
Token fingerprinting end-to-end: HMAC-SHA256 fgp claim embedded in every access token; validated in get_current_user with hmac.compare_digest; login+refresh pass headers; all 4 FGP tests passing
Performance
- Duration: ~35 min
- Started: 2026-06-06T20:00:00Z
- Completed: 2026-06-06T20:35:00Z
- Tasks: 3
- Files modified: 11
Accomplishments
- Added
_compute_fgp(user_agent, accept_lang)helper tobackend/services/auth.py- Returns 16-char hex HMAC-SHA256 prefix using
settings.secret_keyas key - Uses existing
import hmacandimport hashlib— no new imports needed
- Returns 16-char hex HMAC-SHA256 prefix using
- Extended
create_access_tokensignature withuser_agent: str = ""andaccept_lang: str = ""- Backward-compatible defaults (D-05)
- Embeds
"fgp": _compute_fgp(user_agent, accept_lang)in JWT payload
- Added fgp validation block to
get_current_userinbackend/deps/auth.py- Added
import hmac - Placed after
user_nbfcheck and before UUID parse - Empty
fgp_claim→ skip (migration grace for pre-7.4 tokens, D-06) - Non-empty
fgp_claim→ recompute and compare withhmac.compare_digest - Mismatch → HTTP 401 "Token fingerprint mismatch" (D-03)
- Not wrapped in try/except — pure computation, no I/O (T-07.4-04 mitigated)
- Added
- Updated both
create_access_tokencall sites inbackend/api/auth.py- Login handler: passes
User-AgentandAccept-Languageheaders - Refresh handler: same header passthrough
- Login handler: passes
- Promoted all 4 xfail stubs in
test_auth_fgp.pyto real assertions (0 xfail → 4 passed) - Version bumped to 0.1.3 (backend/main.py and frontend/package.json)
- Full pytest suite: 404 passed, 4 skipped, 7 xfailed, 0 failed
Task Commits
- Task 1: Add _compute_fgp + extend create_access_token -
25c9142 - Task 2: Add fgp validation block to get_current_user -
1420180 - Task 3: Update call sites, promote tests, version bump (+ Rule 1 fix) -
61b1e04
Files Created/Modified
backend/services/auth.py— added_compute_fgphelper + extendedcreate_access_tokensignature + fgp payload keybackend/deps/auth.py— addedimport hmac+ fgp validation block after user_nbf checkbackend/api/auth.py— login and refresh call sites updated to pass User-Agent + Accept-Language headersbackend/tests/test_auth_fgp.py— 4 xfail stubs promoted to real passing integration testsbackend/tests/conftest.py—_TEST_USER_AGENTconstant + updated async_client fixture + updated auth_user/second_auth_user/admin_user fixturesbackend/tests/test_auth_deps.py— import_TEST_USER_AGENT; updated auth_client fixture + all create_access_token callsbackend/tests/test_cloud.py— import_TEST_USER_AGENT; updated_create_user_and_tokenhelperbackend/tests/test_documents.py— updated 3 inline create_access_token callsbackend/tests/test_security_headers.py— import_TEST_USER_AGENT; updated headers_client + token creationbackend/main.py— version 0.1.2 → 0.1.3frontend/package.json— version 0.1.2 → 0.1.3
Decisions Made
_compute_fgpis defined inservices/auth.py(service layer) and accessed fromdeps/auth.pyvia the already-importedauth_service._compute_fgp— avoids circular import and keeps fingerprint logic centralized in the service layer- fgp validation block is NOT wrapped in try/except because
_compute_fgpis pure computation with no I/O infrastructure that could fail; unlike the Redis user_nbf check, there is no "fail-open" scenario needed _TEST_USER_AGENT = "docuvault-test/1.0"constant added to conftest.py; all test clients and token-issuing fixtures use this constant to ensure fgp consistency across the test suite
Deviations from Plan
Auto-fixed Issues
1. [Rule 1 - Bug] Test infrastructure fgp mismatch: httpx default User-Agent vs empty-string fgp binding
- Found during: Task 3 verification
- Issue: When running
pytest tests/test_auth_api.py tests/test_auth_deps.py, 10 tests failed with 401. The root cause:auth_user,admin_user,second_auth_userfixtures callcreate_access_token(...)withoutuser_agent, so tokens are bound tofgp(""). Buthttpx.AsyncClientsendsUser-Agent: python-httpx/0.28.1by default. The fgp check inget_current_userrecomputed the fingerprint from this non-empty User-Agent and found it didn't match the token'sfgp("")→ 401. - Fix: Added
_TEST_USER_AGENT = "docuvault-test/1.0"to conftest.py. Updatedasync_clientfixture to send this constant as the User-Agent. Updatedauth_user,second_auth_user,admin_userfixtures to passuser_agent=_TEST_USER_AGENTtocreate_access_token. Updatedtest_auth_deps.py,test_cloud.py,test_documents.py, andtest_security_headers.pyto use the same constant. - Files modified:
backend/tests/conftest.py,backend/tests/test_auth_deps.py,backend/tests/test_cloud.py,backend/tests/test_documents.py,backend/tests/test_security_headers.py - Commit:
61b1e04(part of Task 3 commit)
2. [Rule 1 - Bug] FGP-04 test logic: httpx sends default User-Agent even when not specified
- Found during: Task 3 first test run
- Issue:
test_missing_headers_empty_string_bindingcreated a token with empty-string fgp (no user-agent args) and sent a request "with no User-Agent". But httpx.AsyncClient addsUser-Agent: python-httpx/0.28.1automatically → fgp mismatch → 401 instead of 200. - Fix: Updated the test to explicitly set
"User-Agent": ""in the request headers, matching what the server would compute (_compute_fgp("", "")) and the token's fgp claim. - Files modified:
backend/tests/test_auth_fgp.py - Commit:
61b1e04
Known Stubs
None. All 4 FGP tests are real assertions producing passing results.
Threat Flags
None — this plan implements the mitigations described in the threat model:
- T-07.4-01 (token replay): fgp mismatch now returns 401
- T-07.4-02 (timing attack): hmac.compare_digest used
- T-07.4-04 (exception swallowing): fgp block outside try/except
Self-Check: PASSED
backend/services/auth.pycontainsdef _compute_fgpand"fgp": _compute_fgp(user_agent, accept_lang)backend/deps/auth.pycontainsimport hmac,"Token fingerprint mismatch",fgp_claim = payload.get("fgp", ""),hmac.compare_digest(fgp_claim, fgp_actual)backend/api/auth.pyshows 2 matches foruser_agent=request.headersbackend/main.pyversion is 0.1.3frontend/package.jsonversion is 0.1.3- Commits
25c9142,1420180,61b1e04exist in git log pytest tests/test_auth_fgp.py -vreports 4 PASSED- Full suite: 404 passed, 4 skipped, 7 xfailed, 0 failed