Files
nick-doc/04 - Flows/Authentication Flow.md
Siavash Sameni a1f056e6a5 docs: align flow docs with code reality + create 35 implementation issue files
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>
2026-05-29 14:47:49 +04:00

210 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Authentication Flow
tags: [flow, auth, jwt, login, security]
related_models: ["[[User]]", "[[TempVerification]]"]
related_apis: ["[[Auth API]]", "POST /api/auth/login", "POST /api/auth/refresh-token", "POST /api/auth/logout"]
---
> [!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.
# Authentication Flow
End-to-end specification for **email + password** authentication, JWT issuance, token lifecycle, refresh-token rotation, and logout cleanup. This is the most-used auth path in the marketplace and underpins every protected API call and Socket.IO room subscription.
## Actors
- **User (Buyer / Seller / Admin)** submits credentials via the frontend.
- **Frontend (Next.js)** the JWT sign-in view in `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`, which delegates to `signInWithPassword()` in `frontend/src/auth/context/jwt/action.ts`.
- **Backend (Express)** `AuthController.login` in `backend/src/services/auth/authController.ts`.
- **MongoDB** `User` collection (refresh tokens are appended to `user.refreshTokens[]`).
- **Redis** session store (`sessionService.createSession`) and login-attempt rate-limiter (`rateLimitService.checkLoginAttempts`).
- **Socket.IO** not directly involved in login, but the issued JWT is later used to join `user-${userId}` rooms (see [[Notification Flow]] and [[Chat Flow]]).
## Preconditions
- The user has already completed [[Registration Flow]] and their `User.isEmailVerified === true`.
- The user's `User.status === "active"` (soft-deleted accounts have status `"deleted"`).
- Network connectivity is available (the frontend uses `NetworkUtils.isOnline()` from `frontend/src/auth/utils/error-handler.ts`).
- `localStorage` is available — the frontend rejects the request early via `StorageUtils.isAvailable()` if storage is blocked.
- Backend env vars `JWT_SECRET`, `JWT_EXPIRES_IN`, `REFRESH_TOKEN_EXPIRES_IN` are set (`backend/src/services/auth/authService.ts:19-21`).
## Step-by-step narrative
1. **User fills the sign-in form** at `/auth/jwt/sign-in` and clicks "Sign in". The form is implemented in `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`.
2. **Client-side guards**: `signInWithPassword()` (`action.ts:32-116`) verifies the browser is online and `localStorage` is writable; otherwise it throws a typed `AuthErrorHandler` error.
3. **HTTP request**: The frontend POSTs `{ email, password }` to `POST /api/auth/login` (resolved by `endpoints.auth.login` in `frontend/src/lib/axios.ts`). An `AbortController` is armed with a 60-second timeout.
4. **Validation middleware** runs `loginValidation` (`backend/src/services/auth/authValidation.ts`) — wires into Express via `authRoutes.ts:22`.
5. **Rate limiting**: `rateLimitService.checkLoginAttempts(email, 5, 15*60)` is called (`authController.ts:173`). The counter is incremented on **every login attempt** (before password comparison), not only on failures. Once 5 total attempts accumulate within a 15-minute window, the endpoint returns `429 TOO_MANY_ATTEMPTS`. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts.
6. **User lookup**: `User.findOne({ email, status: "active" }).select("+password")``password` is `select: false` by default in the schema and must be explicitly projected.
7. **Password comparison**: `authService.comparePassword()` invokes `bcrypt.compare()` (cost factor 12 — see `authService.ts:102-105`). Constant-time per bcrypt's design.
8. **Email-verification gate**: If `!user.isEmailVerified`, returns `403 EMAIL_NOT_VERIFIED` with `needsVerification: true`. The frontend intercepts this in `action.ts:104-111` and redirects to `/auth/jwt/verify?email=...`.
9. **Reset attempts**: On success, `rateLimitService.resetLoginAttempts(email)` wipes the Redis counter.
10. **Last-login stamp**: `user.lastLoginAt = new Date()` is saved.
11. **Token issuance**:
- **Access token** — `authService.generateToken()` builds a JWT with `{ id, email, role, isEmailVerified, iat }`, signed with `HS256`, `issuer: 'marketplace-backend'`, `audience: 'marketplace-users'`, default `expiresIn` from config.
- **Refresh token** — `authService.generateRefreshToken()` issues a separate JWT with `{ id, type: 'refresh' }` and a longer TTL.
12. **Refresh-token persistence**: The new refresh token is appended to the `User.refreshTokens` array (`authController.ts:230-231`). This array is the server-side allow-list — only tokens present here can be used to mint new access tokens.
13. **Redis session**: `sessionService.createSession(accessToken, userId, email, role, ip, userAgent, 86400)` stores a 24h session record keyed by the access token (`authController.ts:235-247`). Failures are logged but do **not** block login.
14. **Response**: `200 OK` with `{ user: user.toJSON(), tokens: { accessToken, refreshToken } }`. `toJSON()` strips `password`, `refreshTokens`, and verification codes (see User model `toJSON` transform).
15. **Client-side storage**: The frontend writes both tokens to `localStorage` via `StorageUtils.safeSet()` — keys `accessToken` and `refreshToken` (`action.ts:77-82`).
> [!warning] Token storage is `localStorage`, not cookies
> Tokens are persisted in `window.localStorage`. This is intentional (the API is a separate origin and the app is fully SPA-like), but it means tokens are reachable from any script running on the page. Mitigations in place: strict CSP via `helmet`, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are **no httpOnly auth cookies**.
16. **Axios interceptor** (`frontend/src/lib/axios.ts`) attaches `Authorization: Bearer ${accessToken}` to every subsequent request. On a `401` response, the interceptor automatically triggers the refresh flow described below. A `403` response (e.g., `EMAIL_NOT_VERIFIED`) is **not** retried via refresh — it is surfaced directly to the caller.
17. **Socket.IO bootstrap**: After login, the dashboard layout connects to Socket.IO and emits `join-user-room`, `join-buyer-room`/`join-seller-room` based on `user.role`. See `backend/src/app.ts:83-126`.
## Sequence diagram
```mermaid
sequenceDiagram
autonumber
actor U as User
participant FE as Frontend (Next.js)
participant BE as Backend (Express)
participant DB as MongoDB
participant R as Redis
participant IO as Socket.IO
U->>FE: Enter email + password, click Sign in
FE->>FE: NetworkUtils.isOnline() / StorageUtils.isAvailable()
FE->>BE: POST /api/auth/login { email, password }
BE->>R: rateLimitService.checkLoginAttempts(email)
R-->>BE: { allowed: true, remaining }
BE->>DB: User.findOne({ email, status: "active" }).select("+password")
DB-->>BE: user document
BE->>BE: bcrypt.compare(password, user.password)
alt password invalid
BE-->>FE: 401 Invalid credentials
else email not verified
BE-->>FE: 403 EMAIL_NOT_VERIFIED
FE-->>U: Redirect /auth/jwt/verify
else success
BE->>R: rateLimitService.resetLoginAttempts(email)
BE->>DB: set user.lastLoginAt = now
BE->>DB: save new refresh token
BE->>BE: generateToken(authUser) / generateRefreshToken(authUser)
BE->>R: sessionService.createSession(accessToken, ...)
BE-->>FE: 200 { user, tokens: { accessToken, refreshToken } }
FE->>FE: localStorage.setItem('accessToken' / 'refreshToken')
FE->>IO: socket.emit('join-user-room', userId)
FE-->>U: Redirect to /dashboard
end
```
## API calls
| Method | Endpoint | Source |
|---|---|---|
| `POST` | `/api/auth/login` | `backend/src/services/auth/authRoutes.ts:22``authController.login` |
| `POST` | `/api/auth/telegram` | `authRoutes.ts``authController.telegramAuth` |
| `POST` | `/api/auth/refresh-token` | `authRoutes.ts:24-27``authController.refreshToken` |
| `POST` | `/api/auth/logout` | `authRoutes.ts:68``authController.logout` (protected) |
| `GET` | `/api/auth/profile` | `authRoutes.ts:69``authController.getProfile` |
## Telegram first-class auth flow
Telegram is now a peer auth provider alongside email/password, Google, and passkeys.
1. The Telegram Mini App shell reads raw signed launch data from `window.Telegram.WebApp.initData`; browser login can submit a Telegram Login Widget payload.
2. The frontend posts the raw signed payload to `POST /api/auth/telegram`; it never trusts `initDataUnsafe` for authentication.
3. Backend verifies the Telegram signature:
- Mini App uses `verifyMiniAppInitData()` with the Telegram WebAppData HMAC algorithm.
- Login Widget uses `verifyTelegramLoginWidget()` with the Telegram Login Widget HMAC algorithm.
4. Backend rejects stale `auth_date`, bot accounts, replayed Mini App `initData`, and blocked `TelegramLink` records.
5. If an active `TelegramLink` exists, backend signs in that Amanat user. If no link exists, backend creates a Telegram-only user with nullable email, `authProvider: "telegram"`, `telegramVerified: true`, then creates the link.
6. Backend issues the standard access token and refresh token pair. The frontend stores them in `localStorage` under `accessToken` and `refreshToken`, then hydrates the current session.
7. If `isNewUser=true`, the Mini App opens a lightweight onboarding overlay and points the user to account settings for optional email, language, currency, and wallet details.
High-risk actions are unchanged: escrow release, refund, dispute-sensitive, and wallet-sensitive operations still use the existing protected backend authorization and step-up gates. Telegram auth only establishes the user session.
## Database writes
- **`users` collection**: `lastLoginAt` updated; `refreshTokens` array gains one entry per successful login or refresh.
- No write at all on a failed attempt (only Redis counter increments).
- On `changePassword` / `resetPassword`: `refreshTokens` is reset to `[]` (forces re-login on every device — `authController.ts:600` and `:685`).
## Socket events emitted
- Login itself emits nothing. After the response, the frontend emits the **client-side** events `join-user-room`, `join-buyer-room` or `join-seller-room` to subscribe to targeted notifications.
## Side effects
- **Redis session**: 24-hour key holding `{ userId, email, role, ip, userAgent }`. Used for forced logout (admin can call `sessionService.deleteSession(token)`).
- **Redis rate-limit counter**: TTL 15 min, reset on success. Counter increments on every attempt regardless of outcome.
- **No email** is sent on a normal login (no "new sign-in" notification today — opportunity for future enhancement).
- **Sentry**: any unexpected exception bubbles to `Sentry.setupExpressErrorHandler` (`app.ts:351`).
## Refresh-token flow
The access token is short-lived. When a protected request returns `401 TOKEN_INVALID`, the axios interceptor calls:
1. `POST /api/auth/refresh-token` with `{ refreshToken }` from `localStorage`.
2. Backend `authController.refreshToken` (`:263-313`) verifies the token via `verifyRefreshToken`, checks it is **still present in `user.refreshTokens[]`**, then issues a brand-new access **and** refresh token.
3. The old refresh token is **removed** from the array and the new one is pushed — implementing **refresh-token rotation**. A leaked-but-stale token therefore becomes invalid the moment the legitimate user refreshes.
4. The new pair is written back to `localStorage` and the original failed request is retried.
> [!note] 403 responses are not retried
> The interceptor only triggers token refresh for `status === 401`. A `403` (e.g., `EMAIL_NOT_VERIFIED`) is passed through directly to the caller without attempting a refresh.
> [!warning] Refresh-token sequence diagram is truncated
> The Mermaid diagram below is **incomplete** — it was truncated in the original source at the point where the backend checks that the refresh token exists in `user.refreshTokens`. The remaining steps (rotate tokens, persist, respond, retry original request) are described in prose above but are not yet rendered in the diagram.
```mermaid
sequenceDiagram
autonumber
participant FE as Frontend axios
participant BE as Backend
participant DB as MongoDB
FE->>BE: GET /api/marketplace/... (Bearer access)
BE-->>FE: 401 TOKEN_INVALID
FE->>BE: POST /api/auth/refresh-token { refreshToken }
BE->>BE: verifyRefreshToken(refreshToken)
BE->>DB: User.findById(decoded.id)
BE->>DB: ensure refresh token is in user.refreshTokens
Note over BE,DB: (diagram truncated — remaining steps documented in prose above)
```
## Account management
### changePassword (API-only)
`POST /api/auth/change-password` exists on the backend and the `changePassword()` action is defined in `frontend/src/auth/context/jwt/action.ts`. However:
> [!warning] No frontend UI for change-password
> There is **no dashboard page** that renders a change-password form. The feature is **API-only** at this time. Users cannot change their password through the UI; a developer or direct API client must call the endpoint manually.
### deleteAccount
> [!bug] Account deletion is currently broken
> The frontend `deleteAccount` action calls `DELETE /user/profile`, which does **not exist** on the backend. The real backend endpoint is `DELETE /api/auth/account` (requires `password` in the request body and runs `deleteAccountValidation`). Until the frontend is updated to call the correct endpoint, account deletion will always fail with a 404 or routing error.
## Known issues summary
| Issue | Severity | Details |
|---|---|---|
| `deleteAccount` calls wrong endpoint | Bug | Frontend calls `DELETE /user/profile`; backend endpoint is `DELETE /api/auth/account` |
| No change-password UI | Gap | `POST /api/auth/change-password` and `changePassword()` action exist but no dashboard page renders the form |
| Rate limiter counts all attempts | Clarification | Counter increments before password check — 5 total attempts (not 5 failures) triggers lockout |
| Axios interceptor 403 passthrough | Clarification | Interceptor only auto-refreshes on 401; 403 errors are surfaced directly |
| Refresh-token diagram truncated | Doc debt | Mermaid diagram cut off mid-flow; prose description is authoritative |
## Linked flows
- [[Registration Flow]] — prerequisite; user must be verified.
- [[Password Reset Flow]] — alternative credential recovery path.
- [[Notification Flow]] — uses the issued JWT for Socket.IO room subscriptions.
- [[Chat Flow]] — same JWT used for chat room access.
## Source files
- Backend: `backend/src/services/auth/authController.ts`
- Backend: `backend/src/services/auth/authService.ts`
- Backend: `backend/src/services/auth/authValidation.ts`
- Backend: `backend/src/services/auth/authRoutes.ts`
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx`
- Frontend: `frontend/src/auth/context/jwt/action.ts`
- Frontend: `frontend/src/lib/axios.ts`