Files
nick-doc/03 - API Reference/Authentication API.md
Siavash Sameni dceaf82934 audit: 2026-05-30 full-codebase audit — report, issues, docs, runbooks
Full-codebase-audit 2026-05-30 outputs:
- Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md
- 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer).
- Scanner docs from scratch (was zero): architecture, data model, API ref, payment
  flow, operations runbook + repo README.
- Doc-sync updates across API reference, data models, flows, design system.
- Secret Rotation Runbook (08 - Operations) for the exposed credentials.
- Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js.

Issues remain status:open intentionally — the code fixes are uncommitted-then-committed
working-tree changes per repo and aren't "resolved" until merged/deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 18:48:04 +04:00

15 KiB

title, tags
title tags
Authentication API
api
auth
reference

Authentication API

Last updated: 2026-05-30 — Cloudflare Turnstile CAPTCHA added after 3 failed logins (commit b8edbbf)

All endpoints are mounted under /api/auth/* in backend/src/app.ts. The routes file is backend/src/services/auth/authRoutes.ts and the WebAuthn sub-routes are in passkeyRoutes.ts. Controller logic lives in authController.ts and authService.ts.

Two distinct identities are involved: a User (models/User.ts) and a TempVerification document that holds pending registration data until the email code is confirmed. Tokens are signed JWTs (access + refresh) created in authService. See Authentication Flow for the high-level lifecycle diagram.

Token refresh behaviour: The Axios interceptor handles 401 responses to trigger a token refresh. 403 errors are not intercepted and propagate directly to callers.

Registration

POST /api/auth/register

Description: Start a new registration. Creates a TempVerification document and emails a 6-digit verification code. The actual User is only created once the code is verified. Auth required: No Request body:

{
  email: string;
  firstName?: string;      // default "کاربر"
  lastName?: string;       // default "جدید"
  role?: "buyer" | "seller"; // default "buyer"
  password?: string;       // accepted now or at verify-email-code time
  referralCode?: string;   // optional, links to referrer for [[Points API]]
}

Response 200:

{ "success": true, "message": "Verification code sent ...", "data": { "email": "x@y.z" } }

Errors: 400 validation, 409 USER_EXISTS if email already registered. Side effects:

  • Upserts a TempVerification row.
  • Sends emailService.sendVerificationCodeEmail.
  • No socket emission yet (user does not exist). Source: authController.register

POST /api/auth/verify-email-code

Description: Confirms the registration code. Creates the User, deletes the TempVerification, processes any referral, issues JWT tokens, and starts the session in Redis. Auth required: No Request body:

{
  email: string;
  code: string;            // 6 digits (generated by authService.generateVerificationCode())
  password?: string;       // required if not provided at register
}

Response 200:

{
  "success": true,
  "message": "Email verified",
  "data": {
    "user": { "_id": "...", "email": "...", "role": "buyer", ... },
    "tokens": { "accessToken": "...", "refreshToken": "..." }
  }
}

Errors: 400 invalid/expired code, 404 no pending verification, 409 email already taken. Side effects:

  • Creates User, deletes TempVerification.
  • Calls Points API referral hook when referralCode was supplied (emits referral-signup to user-<referrerId>).
  • Stores refresh token in user.refreshTokens.
  • Welcome email via emailService.sendWelcomeEmail.

GET /api/auth/verify-email/:token

Description: Legacy URL-based verification (link in email). Marks isEmailVerified=true. Auth required: No Response 200: { "success": true, "message": "Email verified successfully" } Errors: 400 invalid/expired token.

POST /api/auth/resend-verification

Description: Re-issues the 6-digit code for a pending or unverified user. Auth required: No Request body: { email: string } Response 200: { "success": true, "message": "Verification code resent" } Errors: 400 invalid email, 404 no user/temp record, 429 rate-limited (Redis).

POST /api/auth/force-verify-user

Description: Development-only helper to mark a user verified without going through email. Auth required: No (intended for dev only — gate with env in prod)

Login & sessions

POST /api/auth/login

Description: Email/password login. Validates credentials with bcrypt, signs JWT pair, stores refresh token on user, returns sanitized user object. Auth required: No Request body:

{
  email: string;
  password: string;
}

Response 200:

{
  "success": true,
  "message": "Login successful",
  "data": {
    "user": { "_id": "...", "email": "...", "role": "buyer", "firstName": "..." },
    "tokens": { "accessToken": "...", "refreshToken": "..." }
  }
}

Errors:

  • 400 validation
  • 401 invalid credentials
  • 403 email not verified
  • 423 account locked (after repeated failures, tracked in Redis via rateLimitService)

Cloudflare Turnstile CAPTCHA: After 3 failed login attempts from the same IP within 15 minutes the captchaGate middleware requires a valid cf-turnstile-response token in the request body. Responses when CAPTCHA is required but missing:

{ "success": false, "captchaRequired": true, "message": "..." }

HTTP status: 429. When TURNSTILE_SECRET_KEY is not set (local dev) the gate is skipped.

⚠️ Rate limiter behaviour: The attempt counter increments on every attempt (before password validation), not only on failures. 5 total attempts within 15 minutes triggers lockout — a user burning 5 attempts with typos will be locked out even if they never had a valid password.

Side effects:

  • Updates user.lastLoginAt.
  • Pushes refresh token onto user.refreshTokens.
  • Redis session start via sessionService.

POST /api/auth/telegram

Description: First-class Telegram authentication. Accepts Telegram Mini App initData or a Telegram Login Widget payload, verifies the Telegram signature server-side, and signs the user into Amanat without requiring email or password. Auth required: No Request body:

// Mini App
{ initData: string; role?: "buyer" | "seller" }

// Login Widget
{ loginWidget: { id: string; first_name?: string; username?: string; auth_date: string; hash: string }; role?: "buyer" | "seller" }

Response 200/201:

{
  "success": true,
  "data": {
    "user": { "_id": "...", "authProvider": "telegram", "telegramVerified": true },
    "tokens": { "accessToken": "...", "refreshToken": "..." },
    "isNewUser": true,
    "telegram": { "userId": "10001", "username": "alice", "source": "miniapp" }
  }
}

Errors: 400 missing payload, 401 invalid/stale signature, 403 blocked Telegram account or inactive Amanat account, 409 TELEGRAM_REPLAY reused Mini App initData, 429 rate-limited. Side effects:

  • Creates a Telegram-only User when no active TelegramLink exists. The user has no email, authProvider: "telegram", and telegramVerified: true.
  • Upserts TelegramLink for the Telegram ID and updates last-seen metadata.
  • Stores the refresh token on the user document.
  • Does not expose phone numbers; Telegram phone data is not requested or persisted.

POST /api/auth/refresh-token

Description: Exchanges a refresh token for a new access token. Rotates the refresh token. Auth required: No (refresh token in body) Request body: { refreshToken: string } Response 200: { "success": true, "data": { "tokens": { "accessToken": "...", "refreshToken": "..." } } } Errors: 401 token expired / not present in user record, 403 user disabled.

POST /api/auth/logout

Description: Removes the current refresh token from the user record and clears the Redis session. Auth required: Bearer JWT Response 200: { "success": true, "message": "Logged out" } Side effects: redisService session removed.

Google OAuth

POST /api/auth/google/signup

Description: Verifies a Google ID token, creates a new User (no password), optionally links a referral, returns JWT tokens. Auth required: No Request body:

{
  googleToken: string;
  role?: "buyer" | "seller";
  referralCode?: string;
}

Response 200: Same shape as verify-email-code response. Errors: 400 invalid Google token, 409 email already registered (suggest sign-in instead).

POST /api/auth/google/signin

Description: Verifies a Google ID token and signs in an existing user. Will not create a new account. Auth required: No Request body: { googleToken: string } Response 200: { success, data: { user, tokens } } Errors: 400 invalid token, 404 no user with that Google email.

Passkey / WebAuthn

Routes are nested under /api/auth/ via passkeyRoutes. Service: passkeyService.ts. These routes go directly to the Express backend via the next.config.ts rewrite rule (/api/:path* → backend). No Next.js route handlers exist for passkey paths.

Implementation status: Passkey attestation is fully implemented using @simplewebauthn/server. The registration and authentication flows are production-ready.

POST /api/auth/passkey/authenticate/challenge

Description: Generates a sign-in challenge that the browser/authenticator will sign. No userId required — the assertion will identify the user. Auth required: No Response 200: { "success": true, "challenge": { /* PublicKeyCredentialRequestOptions */ } }

POST /api/auth/passkey/authenticate

Description: Verifies the WebAuthn assertion and, on success, returns a JWT pair. Auth required: No Request body: { challenge, assertion } (assertion is the browser's navigator.credentials.get() output). Response 200: { "success": true, "userId": "...", "user": { ... }, "tokens": { ... } } Errors: 400 missing fields, 404 Passkey not found, 500 verification error.

POST /api/auth/passkey/register/challenge

Description: Generates a registration challenge for the authenticated user. Auth required: Bearer JWT Response 200: { "success": true, "challenge": { /* PublicKeyCredentialCreationOptions */ } }

POST /api/auth/passkey/register

Description: Verifies a new passkey registration and stores the credential on the user. Auth required: Bearer JWT Request body: { challenge, credential } Response 200: { "success": true, "message": "Passkey registered successfully" }

GET /api/auth/passkey/list

Description: Returns the calling user's registered passkeys (id, label, created date). Auth required: Bearer JWT Response 200: { "success": true, "passkeys": [...] }

DELETE /api/auth/passkey/:passkeyId

Description: Removes a passkey by id. Auth required: Bearer JWT Response 200: { "success": true, "message": "Passkey removed successfully" }

Password management

POST /api/auth/request-password-reset

Description: Generates a reset token, stores it on the user, and emails a reset link plus a numeric code. Auth required: No Request body: { email: string } Response 200: { "success": true, "message": "Password reset email sent" } (always returns success to avoid email enumeration). Side effects: emailService.sendPasswordResetEmail; rate-limited per IP via Redis.

POST /api/auth/reset-password

Description: Sets a new password using a token from the reset email. Wipes refresh tokens. Enforces password complexity via passwordResetValidation. Auth required: No Request body:

{
  token: string;
  password: string;        // 6+ chars, mixed case + digit
}

Response 200: { "success": true, "message": "Password updated" } Errors: 400 invalid/expired token or weak password.

POST /api/auth/reset-password-with-code

Description: Alternative reset flow using a 6-digit numeric code instead of a tokenised URL. Auth required: No Request body: { email, code, password } Response 200: { "success": true } ⚠️ No password complexity validation: Unlike POST /api/auth/reset-password (token-based), this endpoint does not run passwordResetValidation. Any non-empty password will be accepted without complexity checks.

POST /api/auth/change-password

Description: Authenticated password change. Auth required: Bearer JWT Request body:

{
  currentPassword: string;
  newPassword: string;     // 6+ chars, mixed case + digit
}

Response 200: { "success": true, "message": "Password updated" } Errors: 400 validation, 401 wrong current password. Side effects: Clears user.refreshTokens (forces re-login on other devices). ⚠️ No frontend UI: This endpoint exists and is functional in the backend, but no frontend page currently exposes a change-password form. It can only be called directly.

Current user / profile

GET /api/auth/profile

Description: Returns the full sanitized User document for the caller. Auth required: Bearer JWT Response 200: { "success": true, "data": { /* User */ } }

PUT /api/auth/profile (and POST /api/auth/update-profile)

Description: Updates the caller's profile (first/last name, phone, bio, website, language/currency preferences). Auth required: Bearer JWT Request body: Partial profile, see updateProfileValidation:

{
  firstName?: string;       // 2-50
  lastName?: string;        // 2-50
  profile?: {
    phone?: string;         // E.164-ish
    bio?: string;           // <=500
    website?: string;       // URL
  };
  preferences?: {
    language?: "en" | "fa" | "ar";
    currency?: "USD" | "EUR" | "IRR" | "AED";
  };
}

Response 200: Updated user. Errors: 400 validation.

Account deletion

DELETE /api/auth/account

Description: Permanently deletes the caller's account after re-authenticating with password. Requires { password } in the request body and runs deleteAccountValidation. Auth required: Bearer JWT Request body: { password: string } Response 200: { "success": true, "message": "Account deleted" } Errors: 401 bad password. Side effects: Removes User document, clears Redis session, cascades configured by dataCleanupService.

⚠️ KNOWN BUG — Frontend calls wrong endpoint: The frontend currently calls DELETE /user/profile instead of DELETE /api/auth/account. Account deletion initiated from the frontend UI will fail or hit the wrong handler.

Error codes summary

HTTP App code Meaning
400 Validation Error express-validator rejected the body
401 Bad credentials / missing token / invalid Telegram signature
403 Email not verified, insufficient role, or blocked Telegram account
409 USER_EXISTS Email already in use
409 TELEGRAM_REPLAY Reused Telegram Mini App initData (replay protection)
423 Account temporarily locked after failed logins
429 Rate-limited (auth tier: 10 req / 15 min / IP)

See Error Codes for the global error shape.