Files
nick-doc/04 - Flows/Password Reset Flow.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

8.4 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Password Reset Flow
flow
auth
password-reset
email
User
POST /api/auth/request-password-reset
POST /api/auth/reset-password-with-code
POST /api/auth/reset-password

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

[!caution] Audit note — last reviewed 2026-05-29 Several discrepancies between frontend code, backend code, and this document were identified and corrected in this revision. See inline ⚠️ markers for known bugs and mismatches.

Password Reset Flow

Self-service password recovery. There are two separate reset endpoints with different security characteristics:

Endpoint Mechanism Password complexity enforced?
POST /api/auth/reset-password-with-code 6-digit emailed code No — no validation middleware
POST /api/auth/reset-password Token-based (link in email) YespasswordResetValidation requires uppercase + lowercase + digit

The primary UI-driven path uses the code-based endpoint. The token-based endpoint is a legacy/alternative variant.

Actors

  • User who has forgotten their password.
  • Frontendfrontend/src/auth/view/jwt/jwt-reset-password-view.tsx (request) and jwt-update-password-view.tsx (submit new password).
  • BackendAuthController.requestPasswordReset and AuthController.resetPasswordWithCode in backend/src/services/auth/authController.ts.
  • MongoDBUser collection (passwordResetCode, passwordResetCodeExpires, refreshTokens).
  • Email serviceemailService.sendPasswordResetCodeEmail.

Preconditions

  • The account exists and status === "active" (deleted accounts are silently treated as non-existent).
  • The user has access to the email inbox associated with the account.
  • A 6-digit code is valid for 1 hour (authController.ts:556).

Step-by-step narrative

  1. User clicks "Forgot password?" on the sign-in page and lands at /auth/jwt/reset-password.
  2. User enters their email and submits.
  3. Frontend POSTs POST /api/auth/request-password-reset { email }.
  4. Backend authController.requestPasswordReset (:542-574):
    • User.findOne({ email, status: "active" }). If absent, returns 200 with the same generic message — no enumeration.
    • Generates a 6-digit code via authService.generateVerificationCode() (Math.floor(100000 + Math.random() * 900000)).
    • Saves passwordResetCode and passwordResetCodeExpires = now + 3_600_000 ms on the user.
    • Calls emailService.sendPasswordResetCodeEmail(email, firstName, code).
  5. Response: 200 "If an account with this email exists, a password reset code has been sent" regardless of outcome.
  6. User receives the email and enters the code + new password on /auth/jwt/update-password.
  7. Frontend POSTs POST /api/auth/reset-password-with-code { email, code, password }.
  8. Backend authController.resetPasswordWithCode (:611-657):
    • Validates code format /^\d{6}$/ — codes of any other length will always fail here.
    • User.findOne({ email, passwordResetCode: code, passwordResetCodeExpires: { $gt: now }, status: "active" }). Mismatch → 400 Invalid or expired reset code.
    • Hashes the new password with bcrypt cost 12. No password complexity validation is applied — weak passwords such as 123456 or aaaaaa are accepted without error.
    • Sets user.password = hashed, clears passwordResetCode and passwordResetCodeExpires, wipes user.refreshTokens = [] to invalidate all existing sessions.
    • Saves.
  9. Response: 200 "Password reset successfully". Frontend redirects to /auth/jwt/sign-in for a fresh login.

Sequence diagram

sequenceDiagram
    autonumber
    actor U as User
    participant FE as Frontend
    participant BE as Backend
    participant DB as MongoDB
    participant MAIL as Email Service

    U->>FE: Click "Forgot password", enter email
    FE->>BE: POST /api/auth/request-password-reset { email }
    BE->>DB: User.findOne({ email, status: "active" })
    alt user found
        BE->>BE: code = generateVerificationCode() [6 digits]
        BE->>DB: user.passwordResetCode = code\nexpires = +1h
        BE->>MAIL: sendPasswordResetCodeEmail(email, firstName, code)
        MAIL-->>U: Email with 6-digit code
    end
    BE-->>FE: 200 "if account exists, code sent"

    U->>FE: Enter code + new password
    FE->>BE: POST /api/auth/reset-password-with-code { email, code, password }
    BE->>BE: isValidVerificationCode(code) [/^\d{6}$/]
    BE->>DB: User.findOne({ email, code, expires>now })
    BE->>BE: bcrypt.hash(password, 12) [no complexity check]
    BE->>DB: user.password = hash\nuser.refreshTokens = []\nclear reset fields
    BE-->>FE: 200 "Password reset successfully"
    FE-->>U: Redirect /auth/jwt/sign-in

API calls

Method Endpoint Source Notes
POST /api/auth/request-password-reset authRoutes.ts:44-47 Sends 6-digit code by email
POST /api/auth/reset-password-with-code authRoutes.ts:54-56 Code-based; no complexity validation
POST /api/auth/reset-password authRoutes.ts:49-52 Token-based variant; enforces complexity via passwordResetValidation

Two-endpoint comparison

[!important] Code-based vs token-based reset endpoints

POST /api/auth/reset-password-with-code (primary UI path)

  • Uses a 6-digit numeric code delivered by email.
  • isValidVerificationCode() validates with /^\d{6}$/.
  • Has no password complexity middleware. Any string is accepted as the new password.

POST /api/auth/reset-password (legacy token-based path)

  • Uses a URL token (link in email) rather than a short code.
  • Enforces password complexity via passwordResetValidation middleware (requires uppercase, lowercase, and a digit).

The two endpoints provide inconsistent security guarantees. Users who reset via the code flow can set a weak password that would be rejected by the token flow.

Database writes

  • users collection: on request, sets passwordResetCode + passwordResetCodeExpires. On submit, replaces password, clears reset fields, and empties refreshTokens.

Socket events emitted

  • None.

Side effects

  • Email: one transactional message containing the 6-digit code.
  • Server-side log: authController.ts:559 console.log includes the generated code in plain text — same hardening note as Registration Flow.
  • Session invalidation: All refresh tokens cleared → all devices forced to re-login after password change. Access tokens still valid until expiry (typically minutes).

Error / edge cases

  • Unknown email → always 200, generic message. No enumeration.
  • Invalid code format400 from isValidVerificationCode guard before DB lookup.
  • Expired code (>1h) → 400 Invalid or expired reset code.
  • Multiple parallel requests → each overwrites the previous passwordResetCode; the latest email wins, prior codes silently invalidated.
  • User attempts reset on deleted account → treated as unknown (no email sent, 200 returned).
  • Email delivery failure → response still 200; user can request again.
  • Access tokens still valid post-reset → unavoidable with stateless JWT; mitigated by short TTL. Critical operations should re-verify password.

[!warning] Plaintext code in logs Same as Registration Flow: the reset code is console.log-ed by the controller in all environments. Restrict log access in production or gate the log behind NODE_ENV !== 'production'.

Known issues summary

Issue Severity Details
No password complexity on code-based reset Security gap POST /api/auth/reset-password-with-code has no complexity middleware; weak passwords accepted
Inconsistent complexity between reset endpoints Security gap Token-based reset enforces complexity; code-based reset does not

Linked flows

Source files

  • Backend: backend/src/services/auth/authController.ts:542-657
  • Backend: backend/src/services/email/emailService.ts (sendPasswordResetCodeEmail)
  • Frontend: frontend/src/auth/view/jwt/jwt-reset-password-view.tsx
  • Frontend: frontend/src/auth/view/jwt/jwt-update-password-view.tsx
  • Frontend: frontend/src/auth/context/jwt/action.ts:181-200, :261-276