- Three-stream audit (security / logic / performance) with 35+ findings derived from actual source code, each with file:line and remediation - Audit Index cross-references criticals across streams into prioritized fix tiers: immediately / before soft launch / before public launch - Telegram Mini App debug handoff documenting what was implemented and all remaining work items with exact file lists and test commands - Updated architecture, data model, auth API, and registration flow docs to reflect Telegram auth, TON wallet, and email verification additions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
13 KiB
title, tags, created
| title | tags | created | ||||
|---|---|---|---|---|---|---|
| Security Architecture |
|
2026-05-23 |
Security Architecture
How identity, authorization, transport, and integrity are handled across the platform.
Important
Read alongside Authentication Flow (user-facing), Passkey (WebAuthn) Flow, and Payment Flow - SHKeeper (webhook HMAC).
1. Threat model — at a glance
| Threat | Mitigation |
|---|---|
| Credential stuffing | bcrypt 12-round hashing + account lockout + rate-limit |
| Session hijacking | Short-lived JWTs (7d), opaque refresh tokens (30d), token rotation |
| CSRF | JWT in Authorization header (not cookie), CORS allow-list |
| XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage |
| SQL/NoSQL injection | Mongoose parameterized queries, no $where strings, schema validation |
| Webhook spoofing | HMAC SHA-256 over body + secret (SHKeeper, Request Network, Telegram), constant-time compare |
| File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node |
| Replay attacks | Per-payment idempotency on providerPaymentId; Telegram initData in-memory replay map; per-request X-Request-Id |
| Account takeover | Email verification required, password reset code expiry (1h), passkey support |
| Phishing | Passkey origin binding (NEXT_PUBLIC_PASSKEY_ORIGIN), email domain pinning |
| Data leakage | Role-gated endpoints, field-level projection (select: false on password), redacted logs |
| Forged Telegram identity | initData HMAC-SHA256 verified server-side; initDataUnsafe never trusted; auth_date max-age enforced |
| Telegram callback replay | In-memory replay map with configurable window in telegramService.ts |
| Blocked Telegram user bypass | TelegramLink.status === 'blocked' returns 403 regardless of unlink/re-link attempts |
2. Authentication layers
2.1 Email + password (primary)
sequenceDiagram
actor U as User
participant FE as Frontend
participant BE as Backend
participant DB as MongoDB
U->>FE: enters credentials
FE->>BE: POST /api/auth/login { email, password }
BE->>DB: User.findOne({ email })
DB-->>BE: user doc (incl. hashed password)
BE->>BE: bcrypt.compare(password, hash)
alt invalid
BE->>DB: increment loginAttempts
BE->>DB: lock account at N
BE-->>FE: 401 / 423 locked
else valid
BE->>BE: sign JWT (7d), refresh (30d)
BE->>DB: store refresh-token id
BE->>DB: clear attempts
BE-->>FE: 200 { user, token, refreshToken }
end
- Password rules enforced by
authValidation.ts: ≥8 chars, mixed case + digit recommended (cite the validator for exact rules). - bcrypt rounds = 12 (
authService.ts). - Lockout: after N failed attempts within window, account locked for cooldown — see
authService.ts:113-145. - Reset code emailed on
passwordResetCoderequest; valid 1h.
2.2 Google OAuth 2.0
- Frontend uses
NEXT_PUBLIC_GOOGLE_CLIENT_IDfor the Sign-In with Google button. - ID token sent to backend →
googleOAuthService.tsverifies via Google's public keys → either links to existing User by email or creates a new one. - See Google OAuth Flow.
2.3 WebAuthn / Passkey
- Standards-based passwordless.
- Backend:
passkeyService.tsorchestrates registration and assertion challenges. - Frontend env:
NEXT_PUBLIC_PASSKEY_RP_NAME=Amn,NEXT_PUBLIC_PASSKEY_RP_ID=<domain>,NEXT_PUBLIC_PASSKEY_ORIGIN=<origin>. - See Passkey (WebAuthn) Flow.
Warning
Dev env files ship
NEXT_PUBLIC_PASSKEY_RP_ID=localhost. In production this MUST be the actual eTLD+1 domain (e.g.,amn.gg) — passkeys are scoped to the RP ID and can't be transferred.
2.4 Telegram (first-class auth provider)
sequenceDiagram
actor U as User (Telegram)
participant FE as Frontend / Mini App
participant BE as Backend
participant DB as MongoDB
U->>FE: opens Mini App (initData available) or clicks Login Widget
FE->>BE: POST /api/auth/telegram { initData | loginWidget }
BE->>BE: verifyMiniAppInitData() or verifyTelegramLoginWidget()
BE->>BE: reject if auth_date stale / replay / bot account
BE->>DB: TelegramLink.findOne({ telegramUserId })
alt link exists and active
BE->>DB: load linked User
else no link — auto-provision
BE->>DB: User.create({ authProvider:"telegram", telegramVerified:true, email:null })
BE->>DB: TelegramLink.create(...)
end
BE->>DB: upsert TelegramLink.lastSeenAt
BE->>BE: generateToken() + generateRefreshToken()
BE-->>FE: 200 { user, tokens, isNewUser }
- Backend source:
backend/src/services/auth/authController.ts(telegramAuth) andbackend/src/services/telegram/telegramService.ts. - Mini App path uses
HMAC-SHA256("WebAppData", BOT_TOKEN)per Telegram spec; Login Widget path usesHMAC-SHA256(data_check_string, SHA256(BOT_TOKEN)). - In-memory replay maps guard against duplicate
initDatasubmissions within a configurable window. - Blocked
TelegramLinkrecords return403— users cannot circumvent by unlinking and re-linking. - Users with
authProvider: "telegram"have nullable email; email-based operations (password reset) are not applicable to them. - See Authentication Flow#Telegram first-class auth flow.
2.5 Refresh-token rotation
- On
POST /api/auth/refresh, the backend:- Verifies the supplied refresh token.
- Issues a NEW access token + a NEW refresh token.
- Invalidates the old refresh token id in MongoDB.
- If the same refresh token is presented twice → all sessions for that user are invalidated (token reuse detection).
3. Authorization (RBAC)
3.1 Roles
| Role | Source | Capabilities |
|---|---|---|
buyer (user) |
default on signup | Create requests, pay, chat, dispute, rate |
seller (owner) |
chosen at signup OR upgraded | Make offers, build templates, run a shop, withdraw |
admin |
seed / manual | Moderate, mediate disputes, manage users/blogs/levels |
support |
seed / manual | Read-only on most data, can reset passwords, escalate |
A single User may be buyer and seller simultaneously (combined role).
3.2 Enforcement points
- Middleware —
authMiddleware(verifies JWT) followed byroleGuard(role)on every route that requires elevation. - Service layer — defensive
assertRole(ctx, 'admin')calls inside critical service methods so even mis-mounted routes can't bypass. - UI —
AuthGuard+EmailVerificationGuard+ role-aware nav (components/nav-section) hide admin/seller menus for users without permission. This is convenience only — never the security boundary.
4. Transport security
- HTTPS terminated upstream (CloudFlare / external Nginx). Internal cluster is HTTP.
- HSTS header set by upstream proxy (recommended
max-age=31536000; includeSubDomains; preload). - CORS — exactly one origin allowed:
config.frontendUrl.credentials: true. - CSP — Helmet default, currently permissive for Web3 popup compatibility (see
frontend/next.config.tssetting COOP=same-origin-allow-popups, COEP=unsafe-none).
5. Webhook integrity
5.1 SHKeeper
sequenceDiagram
participant SHK
participant BE
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: sha256=<hmac>
BE->>BE: hmac = HMAC_SHA256(SHKEEPER_WEBHOOK_SECRET, body)
BE->>BE: crypto.timingSafeEqual(hmac, providedSig)
alt mismatch
BE-->>SHK: 401 Unauthorized
else match
BE->>BE: process payment update
BE-->>SHK: 200 OK
end
- Raw body must be used for HMAC —
express.raw({ type: 'application/json' })is mounted on this route only (before the globalexpress.json()parser). - In dev (
NODE_ENV === 'development') signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod. - Idempotency: identical webhook delivered twice should be no-op. Check by
(providerPaymentId, status)tuple before mutating.
5.2 Request Network
- Webhooks arrive at
/api/payment/request-network/webhookwith anx-request-network-signatureheader. - The backend verifies the signature using
backend/src/services/payment/requestNetwork/signature.tsbefore any state mutation. - The route is mounted before the global
express.json()body parser so raw body bytes are available for signature computation. - The global rate-limit middleware is configured to skip this path to avoid blocking high-frequency payment events.
- Reconciliation service (
requestNetworkReconciliationService.ts) handles replayed or out-of-order webhooks idempotently.
5.3 Telegram Bot webhook
- Webhooks arrive at
/api/telegram/webhookwith anx-telegram-bot-api-secret-tokenheader. - The backend verifies the token with
verifyTelegramWebhookSecret()(telegramService.ts) before processing updates. - A per-update-id in-memory replay map prevents duplicate processing within the configured window.
- The global rate-limit middleware is configured to skip this path.
See Payment Flow - SHKeeper for the SHKeeper full flow.
6. Input validation
- Backend —
express-validatorper route (e.g.,authValidation.ts), centralisedvalidatemiddleware that 422s on failure with{ details: [...] }. - Frontend —
zodschemas via@hookform/resolvers/zod. Same schema can be re-exported to ashared/package for true single-source-of-truth (not yet wired). - Mongoose — schema-level
type,required,enum,min/max, customvalidatefunctions as a last line of defence.
7. File upload safety
- Stored under
uploads/{avatars|documents|products|temp}/— non-executable, served by Nginx (no Node interpretation). - MIME allow-list in
fileService.ts: images for avatars/products, PDFs/docs for evidence. - 5 MB hard cap (
MAX_FILE_SIZE=5242880). - Original filenames hashed → no path traversal, no clobber.
- Recommended: virus scan via ClamAV before exposing to other users (dispute evidence, chat attachments).
8. Secrets management
- Production secrets injected via host
.env, mounted into composeenv_file. - Never log secrets — logger redaction recommended (winston/pino formatter).
.env*files in.gitignore. Repo includes only.env.development/.env.productiontemplates with public values (NEXT_PUBLIC_*).- Rotate
JWT_SECRETinvalidates all existing JWTs — schedule a maintenance window. - Rotate
SHKEEPER_WEBHOOK_SECRETcoordinated with SHKeeper dashboard (set new → verify → remove old).
See Environment Variables for the catalog.
9. Rate limiting & abuse
- Rate limiting is enabled as of 2026-05-24 (
app.ts). - Active tiers:
/api/auth/*— 10 req / 15 min / IP/api/payment/*— 30 req / 15 min / IP/api/ai/*— 20 req / 15 min / IP- global API — 100 req / 15 min / IP (skips
/healthand Request-Network webhooks)
- Counters are in-memory (Redis adapter planned for distributed deploys).
- For chat and notifications, debounce at the client to avoid spamming legitimate emits.
10. Audit logging
The codebase currently uses morgan (HTTP access logs) and ad-hoc logger.info/warn/error. For PCI-adjacent operations (payments) consider:
- Append-only audit log of every payment / payout / refund / role change.
- Include actor (userId), target, action, before/after diff, request id.
- Persist in a separate Mongo collection or external log sink with retention ≥1y.
11. Frontend session storage
- JWT and refresh token stored in
localStorage(per current implementation — cite to verify infrontend/src/lib/). - Risk: XSS = total takeover. Mitigations: strict CSP, no
dangerouslySetInnerHTMLon untrusted content, audit dependencies (yarn audit). - Alternative: store refresh token in
httpOnlycookie and keep only short-lived access token in memory — recommended for production hardening.
12. Hardening checklist (pre-launch)
- Enable rate-limit middleware (done 2026-05-24)
- Enforce Socket.IO JWT authentication (done 2026-05-24)
- Promote refresh tokens to
httpOnlycookies - Replace
localhostpasskey RP ID with production domain - Disable
NEXT_PUBLIC_IS_DEVELOPMENT=trueandENABLE_DEBUG=truein prod build - Verify
NODE_ENV=productionin backend prod env - Pin production Watchtower to versioned tag (not
latest) - Add backend Sentry SDK + source maps
- Rotate all dev-seeded credentials before public launch
- Run
yarn audit/npm auditand triage CVEs - Pentest the payment + dispute flows specifically
- Review every
> [!warning]callout in this vault
Related
- Authentication Flow (includes Telegram first-class auth flow) · Google OAuth Flow · Passkey (WebAuthn) Flow · Password Reset Flow
- Backend Architecture · Frontend Architecture · Real-time Layer
- Payment Flow - SHKeeper — webhook HMAC details
- Environment Variables — secret catalog
- Incident Response — what to do when something goes wrong