11 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Authentication Flow |
|
|
|
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). Five failures within 15 minutes returns429 TOO_MANY_ATTEMPTS. 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 and, on401/403, automatically calls the refresh flow described below. - 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 |
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.
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.
- 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 or 403, 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.
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