Flow docs updated (11 files): - Delivery Confirmation: reversed actor roles (buyer generates, seller verifies), fixed endpoint paths (/delivery-code/generate, /delivery-code/verify) - Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server attestation is implemented; refresh tokens are persisted - Dispute: corrected resolve schema (action enum), removed non-existent statuses, documented security gaps (no role guards on status/resolve/assign), route shadowing, all socket events are TODO stubs - Seller Offer: corrected all endpoint paths, removed 'active' status, documented withdraw dead code, missing seller history page, select-offer notification gap - Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup, added unread-count-update socket event - Authentication: corrected rate limiter (counts all attempts), axios 403 not handled, deleteAccount wrong endpoint bug, changePassword no UI - Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on reset-with-code vs token reset - Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk, PaymentProvider type gap, getProviderIntentEndpoint routing bug - Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths - Purchase Request: added pending_payment/active statuses, fixed sellers/attachments endpoints, corrected socket events, PUT→PATCH bug - Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap Issues created (35 files in Issues/): - 9 security issues (critical) including: dispute privilege escalation ×4, unauthenticated payment/scanner endpoints ×2, SIM_ production bypass, confirm-delivery ownership gap - 26 additional major/critical bugs covering broken endpoints, missing features, data integrity gaps, and frontend-backend mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.1 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Password Reset Flow |
|
|
|
[!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) | Yes — passwordResetValidation 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.
- Frontend —
frontend/src/auth/view/jwt/jwt-reset-password-view.tsx(request) andjwt-update-password-view.tsx(submit new password). - Backend —
AuthController.requestPasswordResetandAuthController.resetPasswordWithCodeinbackend/src/services/auth/authController.ts. - MongoDB —
Usercollection (passwordResetCode,passwordResetCodeExpires,refreshTokens). - Email service —
emailService.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
- User clicks "Forgot password?" on the sign-in page and lands at
/auth/jwt/reset-password. - User enters their email and submits.
- Frontend POSTs
POST /api/auth/request-password-reset { email }. - Backend
authController.requestPasswordReset(:542-574):User.findOne({ email, status: "active" }). If absent, returns200with the same generic message — no enumeration.- Generates a 6-digit code via
authService.generateVerificationCode()(Math.floor(100000 + Math.random() * 900000)). - Saves
passwordResetCodeandpasswordResetCodeExpires = now + 3_600_000 mson the user. - Calls
emailService.sendPasswordResetCodeEmail(email, firstName, code).
- Response:
200 "If an account with this email exists, a password reset code has been sent"regardless of outcome. - User receives the email and enters the code + new password on
/auth/jwt/update-password. - Frontend POSTs
POST /api/auth/reset-password-with-code { email, code, password }. - Backend
authController.resetPasswordWithCode(:611-657):- Validates code format
/^\d{6}$/— a code of any other length (e.g., 8 digits) 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
123456oraaaaaaare accepted without error. - Sets
user.password = hashed, clearspasswordResetCodeandpasswordResetCodeExpires, wipesuser.refreshTokens = []to invalidate all existing sessions. - Saves.
- Validates code format
- Response:
200 "Password reset successfully". Frontend redirects to/auth/jwt/sign-infor 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}$/. An 8-digit code will always fail.- 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
passwordResetValidationmiddleware (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
userscollection: on request, setspasswordResetCode+passwordResetCodeExpires. On submit, replacespassword, clears reset fields, and emptiesrefreshTokens.
Socket events emitted
- None.
Side effects
- Email: one transactional message containing the 6-digit code.
- Server-side log:
authController.ts:559console.logincludes 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 format →
400fromisValidVerificationCodeguard before DB lookup. Note: theauthController.tscomment mentions "8 digits" but the actual implementation generates and validates exactly 6 digits — any 8-digit code will be rejected. - 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,
200returned). - 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 behindNODE_ENV !== 'production'.
[!bug] Controller comment says "8 digits" but code generates 6 The comment in
authController.tsdescribes an 8-digit code, butauthService.generateVerificationCode()usesMath.floor(100000 + Math.random() * 900000), which produces a number in the range 100000–999999 (exactly 6 digits).isValidVerificationCode()enforces/^\d{6}$/. Any 8-digit value sent toreset-password-with-codewill always be rejected. The comment is wrong; the 6-digit implementation and validation are correct and consistent.
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 |
| Controller comment says 8 digits | Doc bug | Comment is wrong; code generates and validates exactly 6 digits |
| Inconsistent complexity between reset endpoints | Security gap | Token-based reset enforces complexity; code-based reset does not |
Linked flows
- Authentication Flow — user re-signs-in after reset.
- Registration Flow — same code-generation utility.
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