Skip to content

Security & Authorization Architecture

This document is the canonical reference for how the Mr. Mentor / MAS backend authenticates callers, authorizes them against the role matrix, protects secrets, verifies inbound webhooks, and records security-relevant audit trails. It is a cross-cutting platform doc: almost every feature in the suite sits behind one of the guards described here. It is written from source on this branch and reflects the actual middleware, services, and entities — not an idealized design.

Status: documented from source on this branch.


Overview

The backend serves many products (Mr. Mentor, Mr. Hire, MAS LMS, Sales CRM, Finance, AI platform, Vendor API) from one Express app. There are three distinct trust boundaries, each with its own authentication scheme:

Boundary Who Credential Guard
Interactive users (students, mentors, admins, sales, HR, finance) Human users via the three frontends JWT access token (15m) + opaque refresh token (7d) authMiddleware + role middlewares
External vendors Third-party lead-ingestion partners Vendor API key vk_live_<prefix>.<secret> vendorAuthMiddleware
Webhooks / internal services Razorpay, Exotel, mas-class-agent Per-integration shared secret or HMAC signature per-route signature checks

The interactive path is the dominant one. A request carries a JWT (header Authorization: Bearer or cookie token/authToken); authMiddleware verifies the signature, re-loads the user from the database on every request (so blocks, role changes, password changes, and session revocations take effect immediately), then role middlewares decide access. A multi-tenant x-platform header (mr-mentor / my-analytics-school / mr-hire) further constrains login.

Personas / roles: USER, ADMIN, FINANCE, EXPERT, SALES, SALES_HEAD, EXTERNAL_HR, BATCH_LEAD, SUPER_MENTOR, COMMUNITY_MANAGER (src/types/UserTypes.ts). ADMIN is the universal-access role and bypasses every guard layer. The former SUPERADMIN role has been retired — existing rows are migrated to admin at startup (see the enum comment in src/types/UserTypes.ts).


Key concepts & entities

Glossary

  • Access token — short-lived (15m) HS256 JWT. Claims: id, email, role, optional sid (session binding), plus iat/exp. Issued by AuthService.generateJwt.
  • Refresh token — opaque randomUUID (not a JWT), 7-day TTL, stored in the refresh_tokens table. One row = one session/device. Rotated on every refresh.
  • sid claim — binds an access token to its refresh-token row so a session can be force-revoked immediately instead of waiting 15m for the access token to expire.
  • Master login — break-glass admin access: the configured master password unlocks any account, but only after a second factor (TOTP) is verified.
  • Challenge token — single-use, 5-minute Redis token issued between master-password success and TOTP verification.
  • Vendor API keyvk_live_<8-char prefix>.<43-char secret>. Only the prefix (lookup) and an HMAC-SHA256 of the secret (peppered) are stored. The full key is shown once.
  • Pepper — server-side HMAC key (VENDOR_API_KEY_PEPPER) so a leaked DB alone cannot validate vendor keys offline.
  • passwordChangedAt — "logout everywhere" watermark: access tokens with iat older than this are rejected.

Main entities (TypeORM)

Entity File Role
User src/entities/User.ts Identity, role, isActive, isVerified, password (select:false), passwordChangedAt, salesHeadId, googleId
RefreshToken src/entities/RefreshToken.ts Session record: token, userId, expiresAt, revoked, revokedAt, replacedByToken
LoginLog src/entities/LoginLog.ts Audit of LOGIN / LOGOUT / MASTER_LOGIN with IP + user agent
VendorApiKey src/entities/VendorApiKey.ts keyPrefix, keyHash, status, canSubmit, canRead, assignedSalesHeadId, usage stats
VendorApiKeyAccessSummary src/entities/VendorApiKeyAccessSummary.ts Per-tuple (key, ip, method, path) request counters
GstAuditLog src/entities/GstAuditLog.ts Append-only audit trail for GST/invoice field changes

Master auth config (password hash + TOTP secret) is not an entity — it lives in Redis (master_config:*) with env-var fallback, managed by MasterAuthService (src/services/MasterAuthService.ts).


Architecture

flowchart TD
    subgraph Clients["Clients"]
        FE["Frontends (mentor / website / hire)"]
        VEND["External Vendor systems"]
        RZP["Razorpay"]
        EXO["Exotel"]
        AGENT["mas-class-agent"]
    end

    subgraph Edge["Express edge (src/app.ts)"]
        CORS["CORS allowlist"]
        HELM["helmet headers"]
        BODY["json + urlencoded (20mb limit)"]
    end

    subgraph Guards["Auth + authorization middleware"]
        AUTH["authMiddleware (JWT verify + DB re-check)"]
        ROLE["role guards (admin / expert / finance / hr / batchLead / salesHeadPage)"]
        VAUTH["vendorAuthMiddleware (API key)"]
        TOTPRL["totpRateLimit"]
        WHSIG["webhook signature / token guards"]
    end

    subgraph Services["Services"]
        AS["AuthService (JWT, refresh rotation, OAuth)"]
        MAS["MasterAuthService (password + TOTP)"]
        ENC["encryption util (AES-256-CBC)"]
        VKEY["vendorApiKey util (HMAC + constant-time)"]
    end

    subgraph Data["Data + infra"]
        DB[("PostgreSQL: users, refresh_tokens, login_logs, vendor_api_keys, gst_audit_log")]
        REDIS[("Redis: master config, challenge tokens, totp rate limits")]
    end

    FE --> CORS --> HELM --> BODY --> AUTH --> ROLE --> Services
    VEND --> VAUTH --> Services
    RZP --> WHSIG
    EXO --> WHSIG
    AGENT --> WHSIG
    FE -. "master login" .-> TOTPRL --> MAS
    AUTH --> DB
    AS --> DB
    MAS --> REDIS
    VAUTH --> DB
    ENC --> DB
    VKEY --> DB

Data model

erDiagram
    USER ||--o{ REFRESH_TOKEN : "owns sessions"
    USER ||--o{ LOGIN_LOG : "generates"
    USER ||--o{ VENDOR_API_KEY : "created_by / assigned head"
    VENDOR_API_KEY ||--o{ VENDOR_API_KEY_ACCESS_SUMMARY : "tracks usage"

    USER {
        uuid id PK
        string email UK
        string password "select false, bcrypt"
        timestamp passwordChangedAt "logout-everywhere watermark"
        string role "USER ADMIN FINANCE EXPERT SALES etc"
        boolean isActive
        boolean isVerified
        string googleId
        uuid salesHeadId FK
    }
    REFRESH_TOKEN {
        uuid id PK
        string token UK "opaque randomUUID"
        uuid userId FK
        timestamp expiresAt "7 days"
        boolean revoked
        timestamp revokedAt
        string replacedByToken "rotation chain"
        string ipAddress
        string userAgent
    }
    LOGIN_LOG {
        uuid id PK
        uuid userId FK
        string action "login logout master_login"
        string ipAddress
        text userAgent
        timestamp timestamp
    }
    VENDOR_API_KEY {
        uuid id PK
        string keyPrefix UK "vk_live_xxxx"
        string keyHash "HMAC-SHA256 peppered"
        string status "ACTIVE DISABLED REVOKED"
        boolean canSubmit
        boolean canRead
        uuid assignedSalesHeadId FK
        uuid createdBy FK
    }
    VENDOR_API_KEY_ACCESS_SUMMARY {
        uuid id PK
        uuid vendor_api_key_id FK
        string ip
        string method
        string path
        int hit_count
    }
    GST_AUDIT_LOG {
        uuid id PK
        string paymentId
        string action "create update issue"
        string field
        text oldValue
        text newValue
        string changedByEmail
    }

API surface

All auth routes are mounted at the app root (no extra prefix) by src/routes/auth.routes.ts. Vendor and finance routes are mounted by src/routes/index.ts.

Method Path Auth/role Purpose
POST /auth/signup public Create account (bcrypt hash), queue token + OTP
POST /auth/verify-otp public Verify 6-digit signup OTP (10-min TTL)
POST /auth/resend-otp public Resend signup OTP
POST /auth/complete-profile authMiddleware Finish profile after OTP
POST /auth/login public (x-platform aware) Email+password login → JWT + refresh; or master-password → TOTP challenge
POST /auth/refresh public (token in body) Rotate refresh token, mint new access token
POST /auth/logout authMiddleware Revoke current session refresh token
POST /auth/logout-all authMiddleware Revoke all refresh tokens for the user
POST /auth/master-totp totpRateLimit Verify TOTP for a master-password challenge
GET /auth/master-totp/setup authMiddleware (admin) Return TOTP secret + QR for setup
POST /auth/master-totp/test authMiddleware Test a TOTP code without logging in
POST /auth/google public Google OAuth ID-token sign-in
POST /auth/forgot-password public Email a 6-digit reset code (15-min TTL)
POST /auth/reset-password public Reset password with code, bump passwordChangedAt
POST /auth/change-password authMiddleware Change password (verifies current)
GET /auth/sessions authMiddleware List active sessions
POST /auth/sessions/revoke authMiddleware Revoke a specific session
GET /auth/master-auth/status authMiddleware (admin) Whether master password + TOTP are configured
POST /auth/master-auth/configure authMiddleware (admin) Set master password + TOTP secret
POST /auth/master-auth/update-password authMiddleware (admin) Update master password only
POST /auth/master-auth/generate-secret authMiddleware (admin) Generate a new base32 TOTP secret
DELETE /auth/master-auth authMiddleware (admin) Delete master auth config
POST /api/v1/vendor/leads vendorAuthMiddleware('submit') Vendor submits a lead
GET /api/v1/vendor/leads vendorAuthMiddleware('read') Vendor lists their leads
GET /api/v1/vendor/leads/:id vendorAuthMiddleware('read') Vendor fetches one lead
POST /api/exotel/call-status shared-secret ?token= Exotel call status callback
POST /api/exotel/sms-status shared-secret ?token= Exotel SMS status callback
* /api/finance/* authMiddleware + financeOnlyMiddleware Finance portal (FINANCE or ADMIN)
* /api/admin/vendor-keys/* authMiddleware + admin Vendor API-key management (mint/disable/revoke)

Note: Razorpay payment verification is not a dedicated route — signatures are verified inside service methods (e.g. TokenService, CourseEnrollmentService, Mas101PapWorkflowService) when the frontend posts back the razorpay_signature.


User journeys

1. Email + password login (happy path)

The dominant authentication path. Note the access token is minted twice: once bare, then re-minted bound to the freshly created session (sid) so it can be revoked immediately.

sequenceDiagram
    participant FE as Frontend
    participant API as auth.controller.login
    participant AS as AuthService
    participant DB as PostgreSQL

    FE->>API: POST /auth/login with email and password and x-platform
    API->>AS: login email password
    AS->>DB: select user with password column
    DB-->>AS: user row
    AS->>AS: bcrypt compare password
    AS-->>API: success with user
    Note over API: if x-platform is mr-hire and role not allowed then reject 403
    API->>AS: generateRefreshToken userId ip userAgent
    AS->>DB: insert refresh_tokens row
    AS-->>API: refreshToken and expiresIn
    API->>AS: generateJwt user bound to sid
    AS-->>API: 15m access token
    API->>FE: set cookies token refreshToken user and return 200 with token
    Note over API: fireLoginHooks awards daily XP non-blocking

2. Master login (break-glass) with TOTP second factor

When a normal password fails but the master password matches, the server issues a challenge token instead of logging in. The caller must then prove possession of the TOTP secret. The challenge is single-use and TTL-bound in Redis; the TOTP endpoint is rate-limited.

sequenceDiagram
    participant FE as Frontend
    participant API as auth.controller
    participant MAS as MasterAuthService
    participant RDS as Redis
    participant DB as PostgreSQL

    FE->>API: POST /auth/login with email and master password
    API->>MAS: isMasterPassword password
    MAS-->>API: true
    API->>DB: load user and check isActive
    API->>MAS: generateChallengeToken email
    MAS->>RDS: set master_challenge token to email ttl 300s
    API-->>FE: 200 requiresTOTP true with challengeToken
    FE->>API: POST /auth/master-totp with challengeToken and totpCode
    Note over API: totpRateLimit allows 5 attempts per ip per 15m
    API->>MAS: verifyChallengeToken token
    MAS->>RDS: get master_challenge token
    RDS-->>MAS: email
    API->>MAS: verifyTOTP totpCode
    MAS-->>API: valid
    API->>MAS: consumeChallengeToken token
    MAS->>RDS: delete master_challenge token
    API->>DB: insert refresh_tokens and write login_logs master_login
    API-->>FE: 200 master login successful with token and refreshToken

3. Authenticated request authorization (every protected call)

authMiddleware does far more than verify a signature — it re-validates the user against the database on every single request, enforcing immediate blocks, password-change logout, and per-session revocation.

sequenceDiagram
    participant FE as Frontend
    participant MW as authMiddleware
    participant DB as PostgreSQL
    participant RG as role guard
    participant CTRL as controller

    FE->>MW: request with Authorization Bearer token or cookie
    MW->>MW: jwt verify with HS256 and JWT_SECRET
    alt token missing or invalid or expired
        MW-->>FE: 401 with errorType TOKEN_EXPIRED or INVALID_TOKEN
    else valid signature
        MW->>DB: load user by id selecting role isActive isVerified passwordChangedAt salesHeadId
        alt user not found
            MW-->>FE: 401 user no longer exists
        else user inactive
            MW-->>FE: 403 account blocked
        else token predates passwordChangedAt
            MW-->>FE: 401 reason PASSWORD_CHANGED
        else sid session revoked without replacement
            MW-->>FE: 401 reason SESSION_REVOKED
        else all checks pass
            MW->>RG: attach req.user and next
            RG->>RG: compare req.user.role to required role
            alt role mismatch
                RG-->>FE: 403 access denied
            else allowed
                RG->>CTRL: next
                CTRL-->>FE: 200 response
            end
        end
    end

4. Refresh-token rotation

Each refresh rotates the token (old revoked, new minted, chained via replacedByToken). A concurrent double-refresh reuses the replacement rather than failing, which avoids spurious logouts during the admin frontend's proactive refresh.

sequenceDiagram
    participant FE as Frontend
    participant API as auth.controller.refreshToken
    participant AS as AuthService
    participant DB as PostgreSQL

    FE->>API: POST /auth/refresh with refreshToken
    API->>AS: rotateRefreshToken oldToken ip userAgent
    AS->>DB: find refresh_tokens by token
    alt not found
        AS-->>API: null
        API-->>FE: 401 invalid or expired
    else expired
        AS->>DB: remove row
        AS-->>API: null
        API-->>FE: 401 invalid or expired
    else already revoked with replacement
        AS->>DB: load replacedByToken row
        AS-->>API: reuse replacement token
    else valid
        AS->>DB: insert new refresh_tokens row
        AS->>DB: mark old revoked and set replacedByToken
        AS-->>API: new refreshToken and userId
    end
    API->>AS: generateJwt user bound to new sid
    API-->>FE: 200 with accessToken and refreshToken

5. Google OAuth sign-in

sequenceDiagram
    participant FE as Frontend
    participant API as auth.controller.googleSignIn
    participant AS as AuthService
    participant G as Google OAuth2Client
    participant DB as PostgreSQL

    FE->>API: POST /auth/google with idToken
    API->>AS: googleSignIn idToken platform
    AS->>G: verifyIdToken with GOOGLE_CLIENT_ID audience
    G-->>AS: payload with email name sub
    AS->>DB: find user by email
    alt new user
        AS->>DB: create user with googleId isVerified true
        AS->>AS: queue token creation and welcome email
    end
    AS->>AS: reject if isActive is false
    AS-->>API: user
    API->>AS: generateRefreshToken and generateJwt
    API-->>FE: 200 set cookies and return token

6. Vendor API-key authentication

External partners authenticate with an API key. The middleware always runs the hash compare (even when the prefix is unknown) to avoid a timing oracle, checks status + permission scope, then logs usage asynchronously after the response finishes.

sequenceDiagram
    participant VEND as Vendor system
    participant MW as vendorAuthMiddleware
    participant DB as PostgreSQL
    participant CTRL as VendorLeadController

    VEND->>MW: request with X-API-Key vk_live_prefix dot secret
    MW->>MW: parseVendorKey splits prefix and secret
    alt malformed
        MW-->>VEND: 401 missing or malformed API key
    else parsed
        MW->>DB: find vendor_api_keys by keyPrefix
        MW->>MW: hashSecret then constantTimeEqual against stored keyHash
        alt no record or hash mismatch
            MW-->>VEND: 401 invalid API key
        else status not ACTIVE
            MW-->>VEND: 403 disabled or revoked
        else missing required scope
            MW-->>VEND: 403 key lacks submit or read permission
        else authorized
            MW->>CTRL: attach req.vendor and next
            CTRL-->>VEND: 200 response
            Note over MW: on response finish upsert access summary and bump counters via setImmediate
        end
    end

7. Razorpay payment verification

Payment callbacks are verified server-side by recomputing the HMAC-SHA256 signature. The token-purchase flow tries multiple signature formulas because order-flow and invoice-flow callbacks differ.

sequenceDiagram
    participant FE as Frontend
    participant API as token.controller.verifyPayment
    participant SVC as TokenService
    participant DB as PostgreSQL

    FE->>API: POST verify with razorpay payment id and order id and signature
    API->>SVC: verifyPayment payload
    SVC->>DB: load TokenPurchase with stored order and invoice ids
    SVC->>SVC: build candidate bodies and compute HMAC-SHA256 with RAZORPAY_KEY_SECRET
    alt no candidate signature matches
        SVC-->>API: invalid signature
        API-->>FE: failure
    else matched
        SVC->>DB: set paymentStatus COMPLETED and credit tokens
        SVC-->>API: success
        API-->>FE: 200
    end

8. Exotel webhook (shared-secret token)

sequenceDiagram
    participant EXO as Exotel
    participant RT as ExotelWebhookRoutes
    participant SVC as ExotelLeadService

    EXO->>RT: POST /api/exotel/call-status with query token
    RT->>RT: verifyWebhookToken compares to EXOTEL_WEBHOOK_TOKEN
    alt token mismatch or unset
        RT-->>EXO: 403 invalid webhook token
    else valid
        RT->>SVC: handleCallStatus body
        SVC-->>RT: handled
        RT-->>EXO: 200 always so Exotel stops retrying
    end

Background jobs & async

  • Refresh-token cleanupAuthService.cleanupExpiredTokens() bulk-removes expired refresh_tokens rows; runs from the cleanup worker (24h schedule). See request-lifecycle-and-middleware.
  • Vendor access loggingvendorAuthMiddleware registers a res.on('finish') handler and defers all DB writes through setImmediate, so usage counters never delay the response. Writes are an atomic upsert on the (vendor_api_key_id, ip, method, path) unique constraint plus a request_count bump.
  • Login auditinglogLogin / logLogout / logMasterLogin write login_logs rows with IP + user agent. Master logins are recorded with action MASTER_LOGIN.
  • Login side effectsfireLoginHooks awards daily XP and re-evaluates gamification badges, fire-and-forget and idempotent per IST day; never blocks or fails login.
  • Email side effects — OTP, password-reset, password-changed, and welcome emails are queued via QueueService (BullMQ) so SMTP latency never blocks auth responses.

No security-specific Socket.IO events exist; socket connections are authenticated separately in src/socket.ts (out of scope here).


External integrations

Integration Env vars Failure / fallback
JWT JWT_SECRET Missing secret → authMiddleware returns 500 and generateJwt throws
Google OAuth GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI Invalid ID token → sign-in throws and returns error
TOTP / 2FA MASTER_PASSWORD, MASTER_TOTP_SECRET (env fallback); Redis master_config:* overrides Not configured → master login path is simply unavailable; isConfigured() gates it
Redis REDIS_HOST, REDIS_PORT totpRateLimit fails open if Redis is down; challenge tokens unavailable if Redis is down
Vendor keys VENDOR_API_KEY_PEPPER (falls back to JWT_SECRET, then a hardcoded default) Without a dedicated pepper, security degrades to JWT-secret strength
Secret encryption LLM_GATEWAY_ENCRYPTION_KEY / PLATFORM_ENCRYPTION_KEY encryption.ts falls back to a hardcoded DEFAULT_SECRET_KEY if unset — see gotchas
Razorpay RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET Unset secret → signature compare always fails (no valid payment)
Exotel EXOTEL_WEBHOOK_TOKEN, BACKEND_PUBLIC_URL Unset token → verifyWebhookToken returns false for every request (webhooks rejected)
mas-class-agent CLASS_AGENT_API_KEY Used both as X-Internal-API-Key header on outbound proxy calls and as the HMAC key for signed classroom URLs (aiClassroom.controller.generateSignedUrl)

Feature flags: master login self-disables when password+TOTP are unconfigured. Exotel webhook handling self-disables (rejects) when EXOTEL_WEBHOOK_TOKEN is unset.


Status lifecycles

Refresh-token / session lifecycle

stateDiagram-v2
    [*] --> Active : login or google sign-in
    Active --> Rotated : POST /auth/refresh
    Rotated --> Active : replacement issued and chained
    Active --> Revoked : logout or session revoke or logout-all
    Active --> Expired : 7 days elapse
    Rotated --> Revoked : old token marked revoked with replacedByToken
    Expired --> [*] : cleanup worker removes row
    Revoked --> [*] : ages out and cleanup removes row

Distinction enforced in authMiddleware: a token revoked by rotation (has replacedByToken) keeps the session alive; a token revoked without a replacement (explicit logout / password change) triggers immediate SESSION_REVOKED rejection.

Vendor API-key lifecycle

stateDiagram-v2
    [*] --> ACTIVE : admin mints key
    ACTIVE --> DISABLED : admin disables temporarily
    DISABLED --> ACTIVE : admin re-enables
    ACTIVE --> REVOKED : admin revokes permanently
    DISABLED --> REVOKED : admin revokes permanently
    REVOKED --> [*]

Account verification stage

stateDiagram-v2
    [*] --> Unverified : signup creates user isVerified false
    Unverified --> OTP_VERIFIED : verify-otp success
    Unverified --> OTP_VERIFIED : google sign-in pre-verified
    OTP_VERIFIED --> Blocked : admin sets isActive false
    Blocked --> OTP_VERIFIED : admin re-activates

RBAC role-permission matrix

ADMIN is the universal-access role and is accepted by every guard. Other roles are scoped. Middleware sources: admin.middleware.ts, expert.middleware.ts, finance.middleware.ts, hrRole.middleware.ts, batchLead.middleware.ts, salesHeadPage.middleware.ts.

Role (enum) Guard middleware Grants access to Bypass behavior
ADMIN (admin) all Everything — admin, expert, finance, HR, batch-lead, sales-head pages Universal: every guard short-circuits true for admin
FINANCE (finance) financeOnlyMiddleware Finance portal only (/api/finance/*) No bypass of other guards
EXPERT (expert) expertMiddleware Mentor/expert endpoints
SALES (sales) route-level checks Sales CRM endpoints
SALES_HEAD (sales_head) requireHeadPagePermission(...) Sales/CRM/tracking/MAS101 pages gated by per-head profile flags Non-head roles pass through; heads checked against SalesHeadProfileService
EXTERNAL_HR (external_hr) hrRoleMiddleware Mr. Hire HR endpoints Allowed set is HR + ADMIN
BATCH_LEAD (batch_lead) batchLeadMiddleware Batch-lead endpoints ADMIN also allowed
SUPER_MENTOR (super_mentor) route-level Elevated mentor features
COMMUNITY_MANAGER (community_manager) route-level Community management features
USER (user) authMiddleware only Own profile, student/learner features Default role
Vendor (not a User role) vendorAuthMiddleware('submit'\|'read') /api/v1/vendor/* per canSubmit/canRead scope Separate trust boundary; API-key based

Platform gate (orthogonal to role): when x-platform: mr-hire, only EXTERNAL_HR and ADMIN may log in / refresh — enforced in login, masterTotpVerify, and refreshToken before any token is issued (MR_HIRE_ALLOWED_ROLES).


Threat / mitigation table

Threat Mitigation Source
Stolen/long-lived access token 15-minute TTL; DB re-validation on every request auth.middleware.ts, AuthService.generateJwt
Compromised account after password change passwordChangedAt watermark rejects older tokens (logout everywhere) auth.middleware.ts, resetPassword/changePassword
Session hijack / stolen device Per-session sid binding allows immediate revoke; logout-all auth.middleware.ts, logout/logoutAll
Refresh-token replay One-time rotation, old token revoked + chained; reuse-after-replacement handled gracefully AuthService.rotateRefreshToken
JWT algorithm confusion Verification pinned to algorithms: ['HS256'] auth.middleware.ts
Blocked user still acting isActive re-checked on every request and at login/refresh/OAuth auth.middleware.ts, AuthService.login/googleSignIn
Brute-force TOTP on master login Redis rate limit 5 attempts / IP / 15m; single-use 5-min challenge token totpRateLimit.middleware.ts, MasterAuthService
Master login without second factor Master password alone only yields a challenge; TOTP required to mint tokens auth.controller.login/masterTotpVerify
Vendor key DB leak Only HMAC-SHA256 (peppered) of secret stored; pepper not in DB vendorApiKey.ts, VendorApiKey entity
Timing oracle on key/prefix existence Always run hash compare; crypto.timingSafeEqual constant-time compare vendorAuth.middleware.ts, constantTimeEqual
Forged payment confirmation Server recomputes Razorpay HMAC-SHA256 against RAZORPAY_KEY_SECRET TokenService, CourseEnrollmentService, Mas101PapWorkflowService
Forged webhook calls Exotel shared-secret ?token= compared to EXOTEL_WEBHOOK_TOKEN; signed classroom URLs HMAC'd ExotelService.verifyWebhookToken, aiClassroom.controller
Password at rest bcrypt (cost 10 for users, 12 for master); password column select:false AuthService, MasterAuthService, User entity
Secret values at rest (LLM keys, WhatsApp, Leegality, platform) AES-256-CBC with random IV per value encryption.ts, LlmGatewayService etc.
Cross-origin abuse CORS allowlist (CORS_ALLOWED_ORIGINS) + credentials:true app.ts
Common web headers attacks helmet() with cross-origin resource policy app.ts
Oversized payload DoS 20mb body limit on json + urlencoded app.ts
GST/invoice tampering Append-only GstAuditLog records every field change with actor email GstAuditLog entity

Edge cases, limits & gotchas

  • token cookie is not HttpOnly. The access-token cookie is set with httpOnly: false (the frontend reads it), while refreshToken is httpOnly: true. This is intentional but means the access token is reachable by client-side JS — XSS could exfiltrate it. The 15-minute TTL is the main mitigation.
  • secure flag depends on NODE_ENV. Cookies are only secure in production; sameSite is always lax.
  • CORS is wide open in development. NODE_ENV === 'development' allows any origin, and Origin: null (file://) is always allowed. Production relies entirely on the CORS_ALLOWED_ORIGINS allowlist.
  • OTP and reset-token stores are in-memory Maps. otpStore and resetTokenStore in AuthService are process-local — they do not survive a restart and are not shared across PM2/cluster workers. OTP TTL 10m, reset-code TTL 15m.
  • totpRateLimit fails open. If Redis is unreachable, the rate limiter calls next() rather than blocking — availability over strictness.
  • Encryption util has a hardcoded fallback key. encryption.ts falls back to 'default-32-byte-key-change-in-prod!!' when no key is provided, and pads/truncates any key to exactly 32 bytes. AES-256-CBC here provides confidentiality but not authentication (no GCM/MAC) — ciphertext integrity is not verified on decrypt. Always set PLATFORM_ENCRYPTION_KEY / LLM_GATEWAY_ENCRYPTION_KEY.
  • Vendor pepper fallback. VENDOR_API_KEY_PEPPER falls back to JWT_SECRET, then to 'change-me-vendor-pepper'. Set a dedicated value in ops.
  • MR_HIRE_ALLOWED_ROLES has a duplicate. The array lists ADMIN twice ([EXTERNAL_HR, ADMIN, ADMIN]) — harmless because membership is checked with .includes, but a leftover from the SUPERADMIN removal (inferred).
  • masterTotpSetup role check is a tautology. The guard reads role !== ADMIN && role !== ADMIN — effectively just "must be ADMIN" (the second clause is dead, another SUPERADMIN-removal leftover) (inferred).
  • req.ip without trust proxy. The app does not configure trust proxy, so vendorAuthMiddleware derives the client IP manually from x-forwarded-for / cf-connecting-ip / x-real-ip. The TOTP rate limiter keys on req.ip which behind a proxy may be the proxy's IP — meaning the rate-limit bucket can be shared across clients (inferred).
  • Legacy tokens without sid skip the per-session revoke check and age out within 15 minutes; they pick up a sid on the next refresh.
  • Master config precedence: Redis (master_config:password_hash, master_config:totp_secret) overrides env vars; MasterAuthService is a singleton loaded lazily and re-checked via ensureLoaded().
  • Auto-sync schema. TypeORM auto-sync is on; the security tables (refresh_tokens, login_logs, vendor_api_keys, gst_audit_log) are created/altered automatically in dev.