Files
chore/docs/reset-password-reference.md
Ryan Kegel 31ea76f013
All checks were successful
Chore App Build and Push Docker Images / build-and-push (push) Successful in 1m4s
feat: enhance child edit and view components with improved form handling and validation
- 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.
2026-02-17 17:18:03 -05:00

6.0 KiB

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)

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)

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:

{
  "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