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>
16 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Authentication Flow |
|
|
|
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.
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 tosignInWithPassword()infrontend/src/auth/context/jwt/action.ts. - Backend (Express) –
AuthController.logininbackend/src/services/auth/authController.ts. - MongoDB –
Usercollection (refresh tokens are appended touser.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()fromfrontend/src/auth/utils/error-handler.ts). localStorageis available — the frontend rejects the request early viaStorageUtils.isAvailable()if storage is blocked.- Backend env vars
JWT_SECRET,JWT_EXPIRES_IN,REFRESH_TOKEN_EXPIRES_INare set (backend/src/services/auth/authService.ts:19-21).
Step-by-step narrative
- User fills the sign-in form at
/auth/jwt/sign-inand clicks "Sign in". The form is implemented infrontend/src/auth/view/jwt/jwt-sign-in-view.tsx. - Client-side guards:
signInWithPassword()(action.ts:32-116) verifies the browser is online andlocalStorageis writable; otherwise it throws a typedAuthErrorHandlererror. - HTTP request: The frontend POSTs
{ email, password }toPOST /api/auth/login(resolved byendpoints.auth.logininfrontend/src/lib/axios.ts). AnAbortControlleris armed with a 60-second timeout. - Validation middleware runs
loginValidation(backend/src/services/auth/authValidation.ts) — wires into Express viaauthRoutes.ts:22. - 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 returns429 TOO_MANY_ATTEMPTS. The counter is reset upon a fully successful login (step 9). Counters live in Redis so they survive restarts. - User lookup:
User.findOne({ email, status: "active" }).select("+password")—passwordisselect: falseby default in the schema and must be explicitly projected. - Password comparison:
authService.comparePassword()invokesbcrypt.compare()(cost factor 12 — seeauthService.ts:102-105). Constant-time per bcrypt's design. - Email-verification gate: If
!user.isEmailVerified, returns403 EMAIL_NOT_VERIFIEDwithneedsVerification: true. The frontend intercepts this inaction.ts:104-111and redirects to/auth/jwt/verify?email=.... - Reset attempts: On success,
rateLimitService.resetLoginAttempts(email)wipes the Redis counter. - Last-login stamp:
user.lastLoginAt = new Date()is saved. - Token issuance:
- Access token —
authService.generateToken()builds a JWT with{ id, email, role, isEmailVerified, iat }, signed withHS256,issuer: 'marketplace-backend',audience: 'marketplace-users', defaultexpiresInfrom config. - Refresh token —
authService.generateRefreshToken()issues a separate JWT with{ id, type: 'refresh' }and a longer TTL.
- Access token —
- Refresh-token persistence: The new refresh token is appended to the
User.refreshTokensarray (authController.ts:230-231). This array is the server-side allow-list — only tokens present here can be used to mint new access tokens. - 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. - Response:
200 OKwith{ user: user.toJSON(), tokens: { accessToken, refreshToken } }.toJSON()stripspassword,refreshTokens, and verification codes (see User modeltoJSONtransform). - Client-side storage: The frontend writes both tokens to
localStorageviaStorageUtils.safeSet()— keysaccessTokenandrefreshToken(action.ts:77-82).
[!warning] Token storage is
localStorage, not cookies Tokens are persisted inwindow.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 viahelmet, no third-party scripts in the auth views, and short access-token TTL with refresh rotation. There are no httpOnly auth cookies.
- Axios interceptor (
frontend/src/lib/axios.ts) attachesAuthorization: Bearer ${accessToken}to every subsequent request. On a401response, the interceptor automatically triggers the refresh flow described below. A403response (e.g.,EMAIL_NOT_VERIFIED) is not retried via refresh — it is surfaced directly to the caller. The interceptor only checksstatus === 401(axios.ts:105); 403 responses are not handled by the interceptor and propagate as errors. - Socket.IO bootstrap: After login, the dashboard layout connects to Socket.IO and emits
join-user-room,join-buyer-room/join-seller-roombased onuser.role. Seebackend/src/app.ts:83-126.
Sequence diagram
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 |
DELETE |
/api/auth/account |
authRoutes.ts:86-89 → authController.deleteAccount (requires password in body, runs deleteAccountValidation) |
Telegram first-class auth flow
Telegram is now a peer auth provider alongside email/password, Google, and passkeys.
- The Telegram Mini App shell reads raw signed launch data from
window.Telegram.WebApp.initData; browser login can submit a Telegram Login Widget payload. - The frontend posts the raw signed payload to
POST /api/auth/telegram; it never trustsinitDataUnsafefor authentication. - 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.
- Mini App uses
- Backend rejects stale
auth_date, bot accounts, replayed Mini AppinitData, and blockedTelegramLinkrecords. - If an active
TelegramLinkexists, 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. - Backend issues the standard access token and refresh token pair. The frontend stores them in
localStorageunderaccessTokenandrefreshToken, then hydrates the current session. - 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.
Passkey auth flow
The frontend registerPasskey and authenticateWithPasskey actions call passkey API endpoints. All passkey API calls are proxied directly to the Express backend via the next.config.ts rewrite rule (/api/:path* → backend). There are no Next.js route handler files (route.ts) for passkey paths — requests travel: browser → Next.js dev server (rewrite) → Express backend.
Database writes
userscollection:lastLoginAtupdated;refreshTokensarray gains one entry per successful login or refresh.- No write at all on a failed attempt (only Redis counter increments).
- On
changePassword/resetPassword:refreshTokensis reset to[](forces re-login on every device —authController.ts:600and:685).
Socket events emitted
- Login itself emits nothing. After the response, the frontend emits the client-side events
join-user-room,join-buyer-roomorjoin-seller-roomto subscribe to targeted notifications.
Side effects
- Redis session: 24-hour key holding
{ userId, email, role, ip, userAgent }. Used for forced logout (admin can callsessionService.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:
POST /api/auth/refresh-tokenwith{ refreshToken }fromlocalStorage.- Backend
authController.refreshToken(:263-313) verifies the token viaverifyRefreshToken, checks it is still present inuser.refreshTokens[], then issues a brand-new access and refresh token. - 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.
- The new pair is written back to
localStorageand the original failed request is retried.
[!note] 403 responses are not retried The interceptor only triggers token refresh for
status === 401(axios.ts:105). A403(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.
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 frontend calls wrong endpoint The frontend
deleteAccountaction callsDELETE /user/profile, which does not exist on the backend. The real backend endpoint isDELETE /api/auth/account(authRoutes.ts:86-89), which requires apasswordfield in the request body and runsdeleteAccountValidationmiddleware. 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; correct backend endpoint is DELETE /api/auth/account (requires password in body) |
| 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 401-only | Clarification | Interceptor only auto-refreshes on status === 401 (axios.ts:105); 403 errors propagate directly to caller |
| 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