Identity, Authentication & Access Control¶
This document describes how the Mr. Mentor / MAS backend identifies a caller, proves who they are (signup + OTP, password login, Google OAuth, refresh-token rotation, an emergency master-password + TOTP path), and decides what they may do (role guards, per-page sales-head permissions, vendor API keys). It also covers the two cross-product gateways — the admin LLM gateway (/admin/mas/llm-gateway) and the credential proxy to mas-class-agent — that ride on top of the same admin JWT.
Status: documented from source on this branch.
Overview¶
Identity & access is the front door for every product in the suite (Mr. Mentor, Mr. Hire, MAS LMS, Sales CRM, Finance, AI platform). A single User row backs all of them; the active product is selected by the x-platform request header rather than by separate accounts.
Authentication produces a short-lived JWT access token (15 minutes, HS256) plus a refresh token (random UUID, 7 days, persisted in refresh_tokens). Every protected request runs through authMiddleware (src/middleware/auth.middleware.ts), which verifies the JWT and re-reads the user from the database so account blocks, password changes, and per-session revocations take effect immediately rather than waiting for the 15-minute token to expire.
Authorization is role based. The UserRole enum (src/types/UserTypes.ts) defines ten roles; ADMIN is the universal-access role that every guard treats as a bypass. Other roles (EXPERT, FINANCE, EXTERNAL_HR, SALES_HEAD, BATCH_LEAD, etc.) are gated by dedicated middleware. External integrators authenticate with a separate vendor API key scheme (vendorAuthMiddleware) that never touches the JWT path.
Personas:
| Persona | Role(s) | How they authenticate | Where they live |
|---|---|---|---|
| Student / learner | USER |
password or Google, OTP-verified | MAS website, mr-mentor frontend |
| Mentor / expert | EXPERT |
password or Google | mr-mentor frontend |
| Admin / operator | ADMIN |
password or Google (+ optional master TOTP) | mr-mentor-frontend admin |
| Finance operator | FINANCE |
password | Finance Portal |
| Recruiter / HR | EXTERNAL_HR |
password or Google, x-platform: mr-hire |
mr-hire frontend |
| Sales head / sales | SALES_HEAD, SALES, COMMUNITY_MANAGER |
password | CRM |
| Batch lead | BATCH_LEAD |
password | LMS admin |
| External system | (no role — vendor key) | X-API-Key: vk_live_… |
Vendor API platform |
Key concepts & entities¶
Glossary
- Access token (JWT) — HS256, signed with
JWT_SECRET, 15-minute TTL, carries{ id, email, role, sid? }. Generated byAuthService.generateJwt(src/services/AuthService.ts). - Refresh token — opaque
randomUUID(), 7-day TTL, one row per active session inrefresh_tokens. Rotated on every/auth/refresh. sidclaim — binds an access token to the refresh token (session) that minted it, so a single session can be force-revoked immediately.passwordChangedAt— a watermark on the user; any access token whoseiatpredates it is rejected (logout-everywhere on password reset/change).- Master auth — an emergency admin login: a configured master password plus a TOTP code, used to impersonate any account without knowing its password. Backed by Redis +
MasterAuthService. - Challenge token — short-lived (5 min, single-use) Redis token issued when a master password is detected; exchanged for a real session after TOTP verification.
x-platformheader —mr-mentor|my-analytics-school(default) |mr-hire. Selects product context and gates the Mr. Hire portal to HR/admin roles.- Vendor API key —
vk_live_<prefix>.<secret>credential for external integrators, hashed and stored invendor_api_keys.
Main TypeORM entities
| Entity | File | Purpose |
|---|---|---|
User |
src/entities/User.ts |
Canonical identity. Holds password (select: false), role, isActive, isVerified, stage, passwordChangedAt, googleId, signupSource, salesHeadId. |
RefreshToken |
src/entities/RefreshToken.ts |
One row per session. token, userId, expiresAt, revoked, revokedAt, replacedByToken, plus ipAddress/userAgent. Helper methods isExpired/isRevoked/isValid. |
LoginLog |
src/entities/LoginLog.ts |
Audit trail. action ∈ LogAction (login, logout, master_login), ipAddress, userAgent, timestamp. |
GoogleAuthTokens |
src/entities/GoogleAuthTokens.ts |
Google Calendar OAuth tokens (access_token, refresh_token, expiry_date, scope). Distinct from login — used for Calendar sync, not sign-in. |
UserLlmKey |
src/entities/UserLlmKey.ts |
Per-user LLM gateway key link (managed by the admin LLM gateway; mentioned here only because the same admin JWT controls it). |
VendorApiKey |
src/entities/VendorApiKey.ts |
External integrator credentials (keyPrefix, keyHash, status, canSubmit, canRead). |
Note: master-auth secrets are not a TypeORM entity. They live in Redis (
master_config:password_hash,master_config:totp_secret) and optionally in env (MASTER_PASSWORD,MASTER_TOTP_SECRET).
Architecture¶
flowchart TD
subgraph Clients["Clients"]
FE["Web frontends (mr-mentor / website / mr-hire)"]
VENDOR["External vendor system"]
ADMINUI["Admin dashboard"]
end
subgraph Routes["Routes (mounted under /api)"]
AR["auth.routes (/auth/*)"]
UR["user.routes (/user, /users)"]
ADR["admin.routes (/admin/*)"]
CPR["credentialProxy.routes (/admin/mas/credentials)"]
end
subgraph MW["Middleware"]
AM["authMiddleware (JWT + DB recheck)"]
ADMW["adminMiddleware / expert / finance / hrRole / batchLead"]
SHP["requireHeadPagePermission"]
TRL["totpRateLimit (Redis)"]
VAM["vendorAuthMiddleware (API key)"]
end
subgraph Ctrl["Controllers"]
AC["AuthController"]
MGC["MasGatewayController"]
CPC["CredentialProxyController"]
end
subgraph Svc["Services"]
AS["AuthService"]
MAS["MasterAuthService"]
GAS["Google OAuth (google-auth-library)"]
MGS["MasGatewayService"]
end
subgraph Data["Data + external"]
DB[("PostgreSQL: users, refresh_tokens, login_logs")]
REDIS[("Redis: master_config, challenge, totp_rate")]
Q["BullMQ email + database queues"]
GOOG["Google Identity"]
GW["LiteLLM gateway"]
AGENT["mas-class-agent"]
end
FE --> AR
FE --> UR
ADMINUI --> ADR
ADMINUI --> CPR
VENDOR --> VAM
AR --> AC
UR --> AM
ADR --> AM --> ADMW
ADR --> SHP
AR -. master-totp .-> TRL --> AC
CPR --> AM --> ADMW --> CPC
AC --> AS
AC --> MAS
AS --> GAS
ADR --> MGC --> MGS
AS --> DB
AS --> Q
MAS --> REDIS
TRL --> REDIS
GAS --> GOOG
MGS --> GW
CPC --> AGENT
AM --> DB
Data model¶
erDiagram
USER ||--o{ REFRESH_TOKEN : "has sessions"
USER ||--o{ LOGIN_LOG : "logged"
USER ||--o{ GOOGLE_AUTH_TOKENS : "calendar oauth"
USER ||--o{ USER_LLM_KEY : "ai gateway key"
USER }o--|| USER : "salesHead"
USER {
uuid id PK
string email UK
string password "select false"
timestamp passwordChangedAt
enum role
string googleId
boolean isVerified
boolean isProfileComplete
boolean isActive
enum stage
string signupSource
uuid salesHeadId FK
}
REFRESH_TOKEN {
uuid id PK
string token UK
uuid userId FK
string ipAddress
string userAgent
timestamp expiresAt
boolean revoked
timestamp revokedAt
string replacedByToken
timestamp createdAt
}
LOGIN_LOG {
uuid id PK
uuid userId FK
enum action
string ipAddress
text userAgent
timestamp timestamp
}
GOOGLE_AUTH_TOKENS {
uuid id PK
uuid userId FK
text access_token
text refresh_token
bigint expiry_date
string scope
}
VENDOR_API_KEY {
uuid id PK
string keyPrefix
string keyHash
enum status
boolean canSubmit
boolean canRead
timestamp lastUsedAt
}
Notable enums / status fields
UserRole(src/types/UserTypes.ts):user,admin,finance,expert,sales,sales_head,external_hr,batch_lead,super_mentor,community_manager. The formersuperadminrole was retired and migrated toadminat startup.UserStage(src/types/UserStage.ts): numeric —SIGNUP = 1,OTP_VERIFIED = 2,PROFILE_COMPLETE = 3.LogAction(src/entities/LoginLog.ts):login,logout,master_login.VendorApiKeyStatus(src/entities/VendorApiKey.ts):ACTIVE,DISABLED,REVOKED.
API surface¶
All auth and user routes are mounted at /api (src/routes/index.ts). Admin routes are mounted at /api/admin; the credential proxy is mounted at /api but its own base path already starts with /admin/mas/credentials.
Auth routes (src/routes/auth.routes.ts)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| POST | /api/auth/signup |
public | Create account (email + password), queue OTP |
| POST | /api/auth/verify-otp |
public | Verify 6-digit OTP, issue session |
| POST | /api/auth/resend-otp |
public | Resend OTP (always returns generic success) |
| POST | /api/auth/complete-profile |
authMiddleware |
Fill name/phone/profession, advance to PROFILE_COMPLETE |
| POST | /api/auth/login |
public | Password login; may return a TOTP challenge |
| POST | /api/auth/refresh |
public (refresh token in body) | Rotate refresh token, mint new access token |
| POST | /api/auth/logout |
authMiddleware |
Revoke current session refresh token |
| POST | /api/auth/logout-all |
authMiddleware |
Revoke every session for the user |
| POST | /api/auth/master-totp |
totpRateLimit |
Exchange challenge + TOTP for a session |
| GET | /api/auth/master-totp/setup |
authMiddleware (+ admin check inside) |
QR / otpauth URI for the master TOTP secret |
| POST | /api/auth/master-totp/test |
authMiddleware (+ admin check inside) |
Validate a TOTP code without logging in |
| POST | /api/auth/google |
public | Google ID-token sign-in / sign-up |
| POST | /api/auth/forgot-password |
public | Email a 6-digit reset code |
| POST | /api/auth/reset-password |
public | Reset password with code; logout everywhere |
| POST | /api/auth/change-password |
authMiddleware |
Change password; revoke all sessions |
| GET | /api/auth/sessions |
authMiddleware |
List active sessions (devices) |
| POST | /api/auth/sessions/revoke |
authMiddleware |
Revoke one session by token |
| GET | /api/auth/master-auth/status |
authMiddleware (admin inside) |
Whether master password / TOTP are configured |
| POST | /api/auth/master-auth/configure |
authMiddleware (admin inside) |
Set master password + TOTP secret |
| POST | /api/auth/master-auth/update-password |
authMiddleware (admin inside) |
Rotate master password |
| POST | /api/auth/master-auth/generate-secret |
authMiddleware (admin inside) |
Generate a fresh base32 TOTP secret + QR |
| DELETE | /api/auth/master-auth |
authMiddleware (admin inside) |
Delete all master-auth config |
User routes (src/routes/user.routes.ts)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/user/profile |
authMiddleware |
Current user profile |
| GET | /api/user-details |
authMiddleware |
User + token balance |
| PUT | /api/user/profile |
authMiddleware |
Update own profile |
| GET | /api/users |
public (no guard) | List users — see gotchas |
| GET | /api/users/stats |
public (no guard) | User statistics |
| GET | /api/users/:id |
public (no guard) | User by id |
| POST | /api/users |
public (no guard) | Create user |
| PUT | /api/users/:id |
public (no guard) | Update user |
| DELETE | /api/users/:id |
public (no guard) | Soft-delete user |
Cross-product gateways (admin JWT)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/admin/mas/llm-gateway/overview |
authMiddleware + adminMiddleware |
LLM spend / usage KPIs |
| GET | /api/admin/mas/llm-gateway/users |
admin | Per-student gateway key table |
| GET | /api/admin/mas/llm-gateway/users/:userId |
admin | One student detail + recent queries |
| PATCH | /api/admin/mas/llm-gateway/users/:userId/limits |
admin | Edit budget / rate limits |
| POST | /api/admin/mas/llm-gateway/users/:userId/block |
admin | Block / unblock key |
| POST | /api/admin/mas/llm-gateway/users/:userId/key |
admin | Mint a key |
| DELETE | /api/admin/mas/llm-gateway/users/:userId/key |
admin | Revoke a key |
| GET | /api/admin/mas/llm-gateway/logs |
admin | Recent gateway request logs |
| GET | /api/admin/mas/llm-gateway/models |
admin | Model catalog + pricing |
| GET/POST/PUT/DELETE | /api/admin/mas/credentials[...] |
authMiddleware + adminMiddleware |
Proxy to mas-class-agent credential admin |
(Endpoints derived from src/routes/auth.routes.ts, src/routes/user.routes.ts, src/routes/admin.routes.ts, and src/routes/credentialProxy.routes.ts; mount prefixes from src/routes/index.ts.)
User journeys¶
1. Signup + OTP verification¶
Signup is intentionally non-blocking: the user row is created synchronously, but OTP email and token creation are fired asynchronously so the response returns fast. The OTP itself is held in an in-memory Map with a 10-minute TTL.
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as PostgreSQL
participant Q as BullMQ
participant MAIL as Gmail SMTP
FE->>AC: POST /api/auth/signup with email and password
AC->>AS: signUp with device info and platform
AS->>DB: check email not taken then save user with bcrypt hash
AS->>Q: queue create-token job
AS-->>AC: saved user
AC->>AS: sendOtp fire and forget
AS->>MAIL: email 6 digit OTP valid 10 minutes
AC-->>FE: 200 signup successful OTP being sent
Note over FE,AC: later — user enters OTP
FE->>AC: POST /api/auth/verify-otp with email and otp
AC->>AS: verifyOtp checks in-memory store
alt OTP valid and not expired
AS->>DB: set isVerified true and stage OTP_VERIFIED
AS->>Q: queue welcome-email on first verify
AS-->>AC: true
AC->>AS: generateRefreshToken then generateJwt bound to sid
AC->>DB: persist refresh token row
AC-->>FE: 200 token and refreshToken plus set cookies
else invalid or expired
AS-->>AC: false
AC-->>FE: 400 invalid OTP
end
2. Password login -> JWT + refresh token¶
AuthService.login returns a typed result so the controller can distinguish "no account", "wrong password", and "Google-only account" and react accordingly. A blocked account (isActive === false) is rejected before any token is issued.
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as PostgreSQL
FE->>AC: POST /api/auth/login with email password and x-platform header
AC->>AS: login email password
AS->>DB: load user with password column selected
alt user missing
AS-->>AC: failure USER_NOT_FOUND
AC-->>FE: 404 no account found
else no password set
AS-->>AC: failure NO_PASSWORD
AC->>AC: check master password path
else password mismatch
AS-->>AC: failure INVALID_PASSWORD
AC->>AC: check master password path
else success
AS-->>AC: success with user
opt platform is mr-hire and role not HR or admin
AC-->>FE: 403 access denied
end
AC->>AS: generateRefreshToken then generateJwt bound to that session
AC->>DB: save refresh token row
AC->>AC: fireLoginHooks for daily XP and badges non-blocking
AC->>AC: createCrossPlatformLead if login platform differs from signup
AC-->>FE: 200 token refreshToken and user plus set cookies
end
3. Google OAuth sign-in¶
The frontend obtains a Google ID token client-side and posts it here. AuthService.googleSignIn verifies it against GOOGLE_CLIENT_ID, then finds-or-creates a pre-verified user.
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant GOOG as Google Identity
participant DB as PostgreSQL
participant Q as BullMQ
FE->>AC: POST /api/auth/google with idToken and x-platform
AC->>AS: googleSignIn idToken platform
AS->>GOOG: verifyIdToken with audience GOOGLE_CLIENT_ID
GOOG-->>AS: payload with email name picture and sub
alt new user
AS->>DB: create user pre-verified stage OTP_VERIFIED
AS->>Q: queue create-token and welcome-email
else existing user
AS->>DB: load user
end
alt account blocked
AS-->>AC: throw blocked error
AC-->>FE: 403 account blocked
else ok
AS-->>AC: user
opt mr-hire portal and role not allowed
AC-->>FE: 403 access denied
end
AC->>AS: generateRefreshToken then generateJwt bound to session
AC-->>FE: 200 token refreshToken and user plus set cookies
end
4. Protected request via authMiddleware¶
Every guarded route runs authMiddleware, which does far more than verify a signature — it re-reads the user and the session from the DB on every call.
sequenceDiagram
participant FE as Frontend
participant AM as authMiddleware
participant DB as PostgreSQL
participant H as Route handler
FE->>AM: request with Authorization Bearer or token cookie
alt token missing
AM-->>FE: 401 authorization token missing
else token present
AM->>AM: jwt verify with HS256 and JWT_SECRET
AM->>DB: load user selecting id email role isActive passwordChangedAt salesHeadId
alt user not found
AM-->>FE: 401 user no longer exists
else account blocked
AM-->>FE: 403 account blocked
else token iat predates passwordChangedAt
AM-->>FE: 401 password changed please login again
else sid session revoked without replacement
AM-->>FE: 401 session signed out elsewhere
else all checks pass
AM->>H: attach req.user and call next
H-->>FE: 200 handler response
end
end
5. Refresh-token rotation¶
The access token lives only 15 minutes; clients refresh proactively. Rotation revokes the old token and links it to the replacement via replacedByToken, which lets concurrent refreshes converge instead of racing into a forced logout.
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as PostgreSQL
FE->>AC: POST /api/auth/refresh with refreshToken in body
AC->>AS: rotateRefreshToken oldToken ip userAgent
AS->>DB: find existing refresh token row
alt token not found
AS-->>AC: null
AC-->>FE: 401 invalid or expired refresh token
else token expired
AS->>DB: remove expired row
AS-->>AC: null
AC-->>FE: 401 invalid or expired refresh token
else token already revoked with replacement
AS->>DB: load replacedByToken row
AS-->>AC: reuse valid replacement token
else token valid
AS->>DB: create new refresh token and mark old revoked with replacedByToken
AS-->>AC: new refreshToken expiresIn and userId
end
AC->>DB: load user and verify isActive
opt mr-hire portal and role not allowed
AC-->>FE: 403 access denied
end
AC->>AS: generateJwt bound to the rotated session
AC-->>FE: 200 accessToken refreshToken and user plus set token cookie
6. Master-password + TOTP emergency login¶
When normal login fails with the wrong password but the supplied password matches the configured master password, the controller does NOT log the user in. It issues a 5-minute single-use challenge token and demands a TOTP code, which is verified at /auth/master-totp behind a per-IP rate limiter. Successful master logins are written to login_logs as master_login.
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant MA as MasterAuthService
participant REDIS as Redis
participant DB as PostgreSQL
FE->>AC: POST /api/auth/login wrong user password equals master password
AC->>MA: isMasterPassword password
MA->>REDIS: compare against master_config password hash
MA-->>AC: true
AC->>DB: ensure target user exists and is active
AC->>MA: generateChallengeToken email
MA->>REDIS: store challenge token 5 minute TTL
AC-->>FE: 200 requiresTOTP true with challengeToken
FE->>AC: POST /api/auth/master-totp with challengeToken totpCode and email
Note over AC: totpRateLimit allows 5 attempts per IP per 15 minutes
AC->>MA: verifyChallengeToken then verifyTOTP
MA->>REDIS: read challenge and validate code with otplib window 1
alt challenge invalid or email mismatch
AC-->>FE: 400 invalid or expired challenge
else TOTP invalid
AC-->>FE: 401 invalid TOTP code
else valid
AC->>MA: consumeChallengeToken single use
AC->>DB: write login_log action master_login
AC->>AC: generateRefreshToken and generateJwt bound to session
AC-->>FE: 200 master login successful plus set cookies
end
7. Password reset (forgot password)¶
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant MAIL as Gmail SMTP
participant DB as PostgreSQL
participant Q as BullMQ
FE->>AC: POST /api/auth/forgot-password with email
AC->>AS: forgotPassword email
AS->>DB: find user
alt user exists
AS->>AS: store 6 digit reset token in memory 15 minute TTL
AS->>MAIL: email reset code
end
AC-->>FE: 200 generic if account exists message
FE->>AC: POST /api/auth/reset-password with email resetToken newPassword
AC->>AS: resetPassword
AS->>AS: validate reset token not expired
alt invalid or expired
AS-->>AC: throw
AC-->>FE: 400 invalid reset token
else valid
AS->>DB: save bcrypt hash and set passwordChangedAt now
AS->>Q: queue password-changed confirmation email
AS-->>AC: ok
AC-->>FE: 200 password reset successful
end
Note over DB: passwordChangedAt invalidates every older access token on next request
8. Change password + logout everywhere¶
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as PostgreSQL
FE->>AC: POST /api/auth/change-password with current and new password
AC->>AS: changePassword email current new
AS->>DB: load user with password column
alt current password wrong
AS-->>AC: throw current password incorrect
AC-->>FE: 400 error
else Google only account
AS-->>AC: throw cannot change password for OAuth account
AC-->>FE: 400 error
else ok
AS->>DB: save new hash and set passwordChangedAt
AS-->>AC: ok
AC->>AS: revokeAllUserRefreshTokens userId
AC-->>FE: 200 password changed logs out everywhere
end
9. Session management (list / revoke / logout)¶
sequenceDiagram
participant FE as Frontend
participant AC as AuthController
participant AS as AuthService
participant DB as PostgreSQL
FE->>AC: GET /api/auth/sessions
AC->>AS: getActiveSessions userId
AS->>DB: load non revoked unexpired refresh tokens
AS-->>AC: sessions with ip userAgent createdAt
AC-->>FE: 200 sessions with isCurrent flag
FE->>AC: POST /api/auth/sessions/revoke with token
alt token equals current session
AC-->>FE: 400 use logout instead
else other session
AC->>AS: revokeSession userId token
AS->>DB: mark revoked
AC-->>FE: 200 session revoked
end
FE->>AC: POST /api/auth/logout with refreshToken
AC->>AS: revoke only current session token else revoke all
AC->>AS: logLogout writes login_log
AC-->>FE: 200 logout clears cookies
10. Role-gated request (admin / expert / finance / HR / sales-head)¶
After authMiddleware attaches req.user, a second guard checks the role. ADMIN is treated as a universal bypass in expertMiddleware, financeOnlyMiddleware, hrRoleMiddleware, and batchLeadMiddleware; adminMiddleware itself permits only ADMIN. Sales heads get an additional per-page check.
sequenceDiagram
participant FE as Frontend
participant AM as authMiddleware
participant RG as Role guard
participant SHP as requireHeadPagePermission
participant DB as PostgreSQL
participant H as Handler
FE->>AM: request to a guarded admin or role route
AM->>DB: verify token and load user
AM->>RG: next with req.user attached
alt user is ADMIN
RG->>H: bypass — admin sees everything
else role matches the guard
RG->>H: allow
else role does not match
RG-->>FE: 403 access denied role required
end
opt sales head page permission
H->>SHP: requireHeadPagePermission for crm or sales
SHP->>DB: load sales head effective permissions
alt permission present
SHP->>H: allow
else missing
SHP-->>FE: 403 no access to this section
end
end
11. Vendor API key request (external integrator)¶
External systems never use the JWT path. vendorAuthMiddleware parses vk_live_<prefix>.<secret>, looks up by prefix, and compares the hash in constant time (it always runs the hash even when the prefix is unknown, to avoid a timing oracle).
sequenceDiagram
participant V as Vendor system
participant VAM as vendorAuthMiddleware
participant DB as PostgreSQL
participant H as Handler
V->>VAM: request with X-API-Key vk_live prefix dot secret
alt key missing or malformed
VAM-->>V: 401 missing or malformed API key
else parsed
VAM->>DB: find key by prefix
VAM->>VAM: constant time compare hash
alt no record or mismatch
VAM-->>V: 401 invalid API key
else status not active
VAM-->>V: 403 disabled or revoked
else missing required permission
VAM-->>V: 403 no submit or read permission
else ok
VAM->>H: attach req.vendor and next
H-->>V: 200 handler response
Note over VAM,DB: on response finish upsert access summary async
end
end
12. Credential proxy to mas-class-agent¶
sequenceDiagram
participant ADMINUI as Admin dashboard
participant AM as authMiddleware
participant ADMW as adminMiddleware
participant CPC as CredentialProxyController
participant AGENT as mas-class-agent
ADMINUI->>AM: request to /api/admin/mas/credentials with admin JWT
AM->>ADMW: verify token then require ADMIN
ADMW->>CPC: forward
CPC->>AGENT: fetch with X-Internal-API-Key shared secret
AGENT-->>CPC: status and body
CPC-->>ADMINUI: pass through status and body verbatim
Note over CPC,AGENT: on network failure returns 502 reach failure
Background jobs & async¶
- OTP, welcome, password-changed emails — enqueued on
emailQueue(BullMQ) viaQueueService; the email worker sends them. Signup OTP is additionally fired directly (fire-and-forget) for speed. - Token creation on signup / Google signup —
addDatabaseJob({ type: 'create-token' })on the database queue, so the wallet row is created without blocking the auth response. - Login hooks (
fireLoginHooks) — after any successful login the controller credits a daily-login XP grant and re-evaluates badges viaStudentProgressService/BadgeService. Idempotent per IST day, errors swallowed. - Cross-platform lead creation — when a
USERlogs into a platform different from theirsignupSource, aLeadrow is created so sales can track them (skipped for staff roles and formr-hire). - Refresh token cleanup —
AuthService.cleanupExpiredTokensdeletes expired rows; invoked by the cleanup worker (24h schedule per the backend CLAUDE.md). - Vendor access summary —
vendorAuthMiddlewareupserts a per-(key, ip, method, path) summary row onres.on('finish')usingsetImmediate, never blocking the response. - Master-auth state — held in Redis (
master_config:*);MasterAuthServiceloads it lazily and caches it in-process.
No Socket.IO events or external webhooks belong to this domain (the recording/meeting socket flow is documented elsewhere).
External integrations¶
| Integration | Used for | Key env vars | Failure / fallback |
|---|---|---|---|
Google Identity (google-auth-library) |
Verify the ID token on /auth/google |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI |
Invalid token throws; controller returns 400 (or 403 if account blocked) |
Gmail SMTP (nodemailer) |
OTP, reset, login-alert, password-changed emails | EMAIL_USER, EMAIL_PASS, EMAIL_FROM, EMAIL_TIMEOUT_MS |
If creds unset, send is skipped with a warning (OTP logged, not fatal). 8s send timeout. |
| Redis | Master-auth secrets, challenge tokens, TOTP rate-limit | REDIS_HOST, REDIS_PORT |
totpRateLimit fails open if Redis is down; master-auth load is best-effort |
| LiteLLM / MAS LLM gateway | Admin LLM gateway dashboard | gateway base URL + key (see LlmGatewayService) |
MasGatewayController returns 503 LLM_GATEWAY_NOT_CONFIGURED when unset |
mas-class-agent |
Credential proxy | CLASS_AGENT_URL, CLASS_AGENT_API_KEY |
Network failure returns 502; the proxy passes through the agent's status/body otherwise |
TOTP / 2FA is provided by otplib (authenticator) with window: 1 (±30s skew) and qrcode for setup. It is used ONLY for the master-auth emergency path, not for ordinary user logins.
Feature flags (src/utils/featureFlags.ts) gate other domains (token request flow, resume-review spend, etc.) and read ENABLE_* env vars; none currently gate the core auth path.
Symmetric encryption (src/utils/encryption.ts) provides AES-256-CBC encryptSecret/decryptSecret helpers (used for stored third-party secrets such as LLM keys). The fallback key DEFAULT_SECRET_KEY must be overridden in production.
Status lifecycles¶
User onboarding stage (UserStage)¶
stateDiagram-v2
[*] --> SIGNUP
SIGNUP --> OTP_VERIFIED : verify OTP or Google sign in
OTP_VERIFIED --> PROFILE_COMPLETE : complete profile
PROFILE_COMPLETE --> [*]
note right of OTP_VERIFIED
Google users start here
pre verified by Google
end note
Refresh token (session) lifecycle¶
stateDiagram-v2
[*] --> Active : generateRefreshToken
Active --> Rotated : refresh — revoked with replacedByToken
Rotated --> [*] : replacement carries session
Active --> Revoked : logout or revokeSession or password change
Active --> Expired : after 7 days
Revoked --> [*]
Expired --> [*] : cleanupExpiredTokens removes row
note right of Rotated
authMiddleware does NOT reject a token whose
sid was rotated — only an explicit revoke
without a replacement forces logout
end note
Master-auth challenge token lifecycle¶
stateDiagram-v2
[*] --> Issued : master password detected
Issued --> Verified : correct TOTP — token consumed
Issued --> Expired : after 5 minutes
Verified --> [*]
Expired --> [*]
note right of Issued
TOTP may be retried while the
challenge is still valid
end note
Edge cases, limits & gotchas¶
- Hardening note (internal). A security/hardening observation for this area is tracked in the team's private notes (
internal/security-and-hardening-notes.md) and is intentionally not published on this site. - OTP and password-reset codes live in in-process Maps, not Redis (
otpStore,resetTokenStoreinsrc/services/AuthService.ts). They do NOT survive a restart and are NOT shared across instances / PM2 workers — an OTP requested on one process cannot be verified on another. OTP TTL is 10 min; reset-code TTL is 15 min. MR_HIRE_ALLOWED_ROLEShas a duplicated entry. Insrc/controllers/auth.controller.tsit is[EXTERNAL_HR, ADMIN, ADMIN]—ADMINis listed twice (a leftover from theSUPERADMINretirement). It is functionally{EXTERNAL_HR, ADMIN}. The Mr. Hire portal admits only those roles; everyone else gets 403 before any token is issued (enforced on login, Google login, refresh, and master-totp).SUPERADMINno longer exists. The enum dropped it; existingsuperadminrows are migrated toadminat startup.FINANCEis the dedicated finance-portal role and does NOT bypass other guards — onlyADMINdoes.- Access-token cookie is not
httpOnly. Thetokenandusercookies are set withhttpOnly: false(readable by JS by design, for the frontends); onlyrefreshTokenishttpOnly. All aresameSite: 'lax'andsecureonly in production. authMiddlewarehits the DB on every request (user lookup + optional session lookup) to enforce blocks, password-change, and session revocation in real time. This is a deliberate trade of latency for immediate revocation.- Per-session revoke only fires on explicit revoke without a replacement. A token rotated normally keeps the session alive (
replacedByTokenset), preventing spurious logouts during proactive refresh; a missing session is ignored and simply ages out within 15 minutes; legacy tokens with nosidare never session-checked. passwordChangedAtcomparison is at second granularity (JWTiatis in seconds) so the fresh re-login token minted in the same second as the change is not rejected.totpRateLimitfails open. If Redis is unavailable the limiter lets the request through (5 attempts / 15 min / IP otherwise).- Master password minimum length is 16 chars, enforced in
masterAuthConfigure/masterAuthUpdatePassword. Redis values override env (MASTER_PASSWORD,MASTER_TOTP_SECRET). - Master-auth admin endpoints check
ADMINtwice (req.user?.role !== UserRole.ADMIN && req.user?.role !== UserRole.ADMIN) — sameSUPERADMIN-removal artefact; effectively admin-only. - Google-only accounts cannot password-login or change password.
AuthService.loginreturnsNO_PASSWORD, andchangePasswordthrows — the UI must steer them to Google. GoogleAuthTokensis not for login. It stores Calendar OAuth tokens; sign-in uses ID-token verification only and storesgoogleIdon the user.- Cross-platform login silently creates a sales
LeadforUSER-role accounts whose login platform differs fromsignupSource; failures are non-blocking.
Related docs¶
- API endpoints reference
- Backend database schema
- Backend design patterns
- System design overview
- Deployment guide
- Sibling feature docs (same folder), when published:
users-and-profiles.md,sales-crm.md,finance-and-payments.md,ai-platform-llm-gateway.md,mr-hire-recruitment.md.