# Password Reset Reference This document explains the full password reset and forced re-auth flow implemented in the project. ## Scope This covers: - reset token validation and reset submission - JWT invalidation after reset - behavior of `/auth/me` with stale tokens - multi-tab synchronization in the frontend --- ## High-Level Behavior After a successful password reset: 1. Backend updates the password hash. 2. Backend increments the user's `token_version`. 3. Backend clears the `token` auth cookie in the reset response. 4. Existing JWTs in other tabs/devices become invalid because their embedded `token_version` no longer matches. 5. Frontend broadcasts a logout sync event so other tabs immediately redirect to login. --- ## Backend Components ### 1) User model versioning File: `backend/models/user.py` - Added `token_version: int = 0`. - `from_dict()` defaults missing value to `0` for backward compatibility. - `to_dict()` persists `token_version`. ### 2) JWT issuance includes token version File: `backend/api/auth_api.py` (`/auth/login`) JWT payload now includes: - `email` - `user_id` - `token_version` - `exp` ### 3) `/auth/me` rejects stale tokens File: `backend/api/auth_api.py` (`/auth/me`) Flow: - decode JWT - load user from DB - compare `payload.token_version` (default 0) with `user.token_version` - if mismatch, return: - status: `401` - code: `INVALID_TOKEN` ### 4) reset-password invalidates sessions File: `backend/api/auth_api.py` (`/auth/reset-password`) On success: - hash and store new password - clear `reset_token` and `reset_token_created` - increment `user.token_version` - persist user - clear `token` cookie in response (`expires=0`, `httponly=True`, `secure=True`, `samesite='Strict'`) ### 5) shared auth utility enforcement File: `backend/api/utils.py` (`get_current_user_id`) Protected endpoints that use this helper also enforce token version: - decode JWT - load user by `user_id` - compare JWT `token_version` vs DB `token_version` - return `None` if mismatch --- ## Frontend Components ### 1) Reset password page File: `frontend/vue-app/src/components/auth/ResetPassword.vue` On successful `/api/auth/reset-password`: - calls `logoutUser()` from auth store - still shows success modal - Sign In action navigates to login ### 2) Cross-tab logout sync File: `frontend/vue-app/src/stores/auth.ts` Implemented: - logout broadcast key: `authSyncEvent` - `logoutUser()`: - applies local logged-out state - writes logout event to localStorage - `initAuthSync()`: - listens to `storage` events - if logout event arrives, applies logged-out state and redirects to `/auth/login` when outside `/auth/*` - `checkAuth()` now funnels failed `/api/auth/me` checks through `logoutUser()` ### 3) Sync bootstrap File: `frontend/vue-app/src/main.ts` - calls `initAuthSync()` at app startup. ### 4) Global `401 Unauthorized` handling Files: - `frontend/vue-app/src/common/api.ts` - `frontend/vue-app/src/main.ts` Implemented: - `installUnauthorizedFetchInterceptor()` wraps global `fetch` - if any response is `401`, frontend: - calls `logoutUser()` - redirects to `/auth` (unless already on `/auth/*`) This ensures protected pages consistently return users to auth landing when a session is invalid. --- ## Sequence Diagram (Reset Success) ```mermaid sequenceDiagram participant U as User (Tab A) participant FE as ResetPassword.vue participant BE as auth_api.py participant DB as users_db participant LS as localStorage participant T2 as Browser Tab B U->>FE: Submit new password + token FE->>BE: POST /api/auth/reset-password BE->>DB: Validate reset token + expiry BE->>DB: Update password hash BE->>DB: token_version = token_version + 1 BE-->>FE: 200 + clear auth cookie FE->>LS: logoutUser() writes authSyncEvent LS-->>T2: storage event(authSyncEvent: logout) T2->>T2: clear auth state T2->>T2: redirect /auth/login ``` --- ## Sequence Diagram (Stale Token Check) ```mermaid sequenceDiagram participant T as Any Tab with old JWT participant BE as /auth/me participant DB as users_db T->>BE: GET /auth/me (old JWT token_version=N) BE->>DB: Load user (current token_version=N+1) BE-->>T: 401 { code: INVALID_TOKEN } ``` --- ## Example API Calls ### Validate reset token `GET /api/auth/validate-reset-token?token=` Possible failures: - `400 MISSING_TOKEN` - `400 INVALID_TOKEN` - `400 TOKEN_TIMESTAMP_MISSING` - `400 TOKEN_EXPIRED` ### Reset password `POST /api/auth/reset-password` Request body: ```json { "token": "", "password": "newStrongPassword123" } ``` Success: - `200 { "message": "Password has been reset" }` - response also clears auth cookie ### Auth check after reset with stale JWT `GET /api/auth/me` Expected: - `401 { "error": "Invalid token", "code": "INVALID_TOKEN" }` --- ## SSE vs Cross-Tab Sync Current design intentionally does **not** rely on SSE to enforce logout correctness. Why: - Security correctness is guaranteed by cookie clearing + token_version invalidation. - SSE can improve UX but is not required for correctness. - Cross-tab immediate UX is handled client-side via localStorage `storage` events. --- ## Test Coverage Backend: - `backend/tests/test_auth_api.py` - includes regression test ensuring old JWT fails `/auth/me` after reset. Frontend: - `frontend/vue-app/src/stores/__tests__/auth.childmode.spec.ts` - includes cross-tab storage logout behavior. - `frontend/vue-app/src/common/__tests__/api.interceptor.spec.ts` - verifies global `401` interceptor logout and redirect behavior. --- ## Troubleshooting Checklist - If stale sessions still appear valid: - verify `token_version` exists in user records - confirm `/auth/login` includes `token_version` claim - confirm `/auth/me` compares JWT vs DB token_version - confirm `/auth/reset-password` increments token_version - If other tabs do not redirect: - verify `initAuthSync()` is called in `main.ts` - verify `logoutUser()` is called on reset success - check browser supports storage events across tabs for same origin