All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m4s
- Added `requireDirty` prop to `EntityEditForm` for dirty state management. - Updated `ChildEditView` to handle initial data loading and image selection more robustly. - Refactored `ChildView` to remove unused reward dialog logic and prevent API calls in child mode. - Improved type definitions for form fields and initial data in `ChildEditView`. - Enhanced error handling in form submissions across components. - Implemented cross-tab logout synchronization on password reset in the auth store. - Added tests for login and entity edit form functionalities to ensure proper behavior. - Introduced global fetch interceptor for handling unauthorized responses. - Documented password reset flow and its implications on session management.
259 lines
6.0 KiB
Markdown
259 lines
6.0 KiB
Markdown
# 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=<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": "<reset-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
|