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, optionalsid(session binding), plusiat/exp. Issued byAuthService.generateJwt. - Refresh token — opaque
randomUUID(not a JWT), 7-day TTL, stored in therefresh_tokenstable. One row = one session/device. Rotated on every refresh. sidclaim — 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 key —
vk_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 withiatolder 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 therazorpay_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 cleanup —
AuthService.cleanupExpiredTokens()bulk-removes expiredrefresh_tokensrows; runs from the cleanup worker (24h schedule). See request-lifecycle-and-middleware. - Vendor access logging —
vendorAuthMiddlewareregisters ares.on('finish')handler and defers all DB writes throughsetImmediate, so usage counters never delay the response. Writes are an atomic upsert on the(vendor_api_key_id, ip, method, path)unique constraint plus arequest_countbump. - Login auditing —
logLogin/logLogout/logMasterLoginwritelogin_logsrows with IP + user agent. Master logins are recorded with actionMASTER_LOGIN. - Login side effects —
fireLoginHooksawards 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 (hasreplacedByToken) keeps the session alive; a token revoked without a replacement (explicit logout / password change) triggers immediateSESSION_REVOKEDrejection.
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¶
tokencookie is not HttpOnly. The access-token cookie is set withhttpOnly: false(the frontend reads it), whilerefreshTokenishttpOnly: 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.secureflag depends onNODE_ENV. Cookies are onlysecurein production;sameSiteis alwayslax.- CORS is wide open in development.
NODE_ENV === 'development'allows any origin, andOrigin: null(file://) is always allowed. Production relies entirely on theCORS_ALLOWED_ORIGINSallowlist. - OTP and reset-token stores are in-memory
Maps.otpStoreandresetTokenStoreinAuthServiceare process-local — they do not survive a restart and are not shared across PM2/cluster workers. OTP TTL 10m, reset-code TTL 15m. totpRateLimitfails open. If Redis is unreachable, the rate limiter callsnext()rather than blocking — availability over strictness.- Encryption util has a hardcoded fallback key.
encryption.tsfalls 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 setPLATFORM_ENCRYPTION_KEY/LLM_GATEWAY_ENCRYPTION_KEY. - Vendor pepper fallback.
VENDOR_API_KEY_PEPPERfalls back toJWT_SECRET, then to'change-me-vendor-pepper'. Set a dedicated value in ops. MR_HIRE_ALLOWED_ROLEShas a duplicate. The array listsADMINtwice ([EXTERNAL_HR, ADMIN, ADMIN]) — harmless because membership is checked with.includes, but a leftover from the SUPERADMIN removal (inferred).masterTotpSetuprole check is a tautology. The guard readsrole !== ADMIN && role !== ADMIN— effectively just "must be ADMIN" (the second clause is dead, another SUPERADMIN-removal leftover) (inferred).req.ipwithouttrust proxy. The app does not configuretrust proxy, sovendorAuthMiddlewarederives the client IP manually fromx-forwarded-for/cf-connecting-ip/x-real-ip. The TOTP rate limiter keys onreq.ipwhich behind a proxy may be the proxy's IP — meaning the rate-limit bucket can be shared across clients (inferred).- Legacy tokens without
sidskip the per-session revoke check and age out within 15 minutes; they pick up asidon the next refresh. - Master config precedence: Redis (
master_config:password_hash,master_config:totp_secret) overrides env vars;MasterAuthServiceis a singleton loaded lazily and re-checked viaensureLoaded(). - 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.
Related docs¶
- Identity & Access — user lifecycle, profiles, stages, roles in depth.
- Vendor API Platform — vendor key minting, scopes, lead ingestion.
- Payments, Finance & GST — Razorpay flows, invoicing, finance audit.
- Request Lifecycle & Middleware — middleware order, CORS, body limits, cleanup jobs.