Skip to content

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 by AuthService.generateJwt (src/services/AuthService.ts).
  • Refresh token — opaque randomUUID(), 7-day TTL, one row per active session in refresh_tokens. Rotated on every /auth/refresh.
  • sid claim — 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 whose iat predates 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-platform headermr-mentor | my-analytics-school (default) | mr-hire. Selects product context and gates the Mr. Hire portal to HR/admin roles.
  • Vendor API keyvk_live_<prefix>.<secret> credential for external integrators, hashed and stored in vendor_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. actionLogAction (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 former superadmin role was retired and migrated to admin at 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) via QueueService; the email worker sends them. Signup OTP is additionally fired directly (fire-and-forget) for speed.
  • Token creation on signup / Google signupaddDatabaseJob({ 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 via StudentProgressService / BadgeService. Idempotent per IST day, errors swallowed.
  • Cross-platform lead creation — when a USER logs into a platform different from their signupSource, a Lead row is created so sales can track them (skipped for staff roles and for mr-hire).
  • Refresh token cleanupAuthService.cleanupExpiredTokens deletes expired rows; invoked by the cleanup worker (24h schedule per the backend CLAUDE.md).
  • Vendor access summaryvendorAuthMiddleware upserts a per-(key, ip, method, path) summary row on res.on('finish') using setImmediate, never blocking the response.
  • Master-auth state — held in Redis (master_config:*); MasterAuthService loads 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, resetTokenStore in src/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_ROLES has a duplicated entry. In src/controllers/auth.controller.ts it is [EXTERNAL_HR, ADMIN, ADMIN]ADMIN is listed twice (a leftover from the SUPERADMIN retirement). 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).
  • SUPERADMIN no longer exists. The enum dropped it; existing superadmin rows are migrated to admin at startup. FINANCE is the dedicated finance-portal role and does NOT bypass other guards — only ADMIN does.
  • Access-token cookie is not httpOnly. The token and user cookies are set with httpOnly: false (readable by JS by design, for the frontends); only refreshToken is httpOnly. All are sameSite: 'lax' and secure only in production.
  • authMiddleware hits 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 (replacedByToken set), preventing spurious logouts during proactive refresh; a missing session is ignored and simply ages out within 15 minutes; legacy tokens with no sid are never session-checked.
  • passwordChangedAt comparison is at second granularity (JWT iat is in seconds) so the fresh re-login token minted in the same second as the change is not rejected.
  • totpRateLimit fails 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 ADMIN twice (req.user?.role !== UserRole.ADMIN && req.user?.role !== UserRole.ADMIN) — same SUPERADMIN-removal artefact; effectively admin-only.
  • Google-only accounts cannot password-login or change password. AuthService.login returns NO_PASSWORD, and changePassword throws — the UI must steer them to Google.
  • GoogleAuthTokens is not for login. It stores Calendar OAuth tokens; sign-in uses ID-token verification only and stores googleId on the user.
  • Cross-platform login silently creates a sales Lead for USER-role accounts whose login platform differs from signupSource; failures are non-blocking.