Appearance
User Authentication Workflow (Detailed)
This document provides a comprehensive, end-to-end view of the user authentication system, including the public auth entry points, session-aware token lifecycle, and the self-service endpoints exposed under /api/identity.
0. Public Email Discovery (Pre-Login Routing)
Before rendering login vs register forms, the frontend can call POST /api/identity/auth/user/check-email with { email } and receive { found: boolean }.
found: trueindicates a user account already exists for that email.found: falseindicates no user account exists for that email.- Endpoint is public and protected with a strict rate limit (
3 req / 60s / IP) to reduce abuse.
1. Registration & Verification
The registration process uses a secure, rate-limited email verification flow.
1.1 Initiate Registration
- Client sends
email,password,firstName,lastNametoPOST /api/identity/auth/user/register. - The system checks for existing emails.
- A new
Userentity is created withisActive: false. - Registration also creates a default owned agency for that user and the initial owner membership so the first authenticated session already has an
agencyIdcontext. - The password is hashed using a strong algorithm.
- Any previous unused verification codes for this user are invalidated.
- A secure 6-digit verification code is generated using
crypto.randomInt(). - The code is hashed with the platform hashing service before storage (no plaintext persistence), with a 15-minute expiration and a tracking counter for attempts (
attempts: 0). - An event is published to send the verification email.
1.2 Verify Email
- Client submits the
code. - The system checks the latest active token for the user.
- Security Check: If the token has reached
MAX_VERIFICATION_ATTEMPTS(5), it is locked. Rate limiting (@RateLimit) prevents brute-force attacks on the endpoint (5 req/min per IP via Redis sliding window). - The input code is validated against the stored hash. During transition, legacy plaintext rows are still accepted.
- On failure,
attemptsis incremented. - On success, the token is marked as used, the User is activated, and a new Session & Token Pair is generated.
- Verify-email responses include tokens plus actor profile (
id,email,firstName,lastName, nullablephone), the current session entry (session), andorganizations(see below).
sequenceDiagram
participant C as Client
participant A as Auth API
participant DB as Database
C->>A: POST /api/identity/auth/user/register
A->>DB: Invalidate old codes
A->>A: Generate CSPRNG code
A->>DB: Store hashed code & User (inactive)
A-->>C: 200 OK (Email Sent)
C->>A: POST /api/identity/auth/user/verify-email {code}
A->>DB: Fetch code hash & attempts
alt Exceeds max attempts
A-->>C: 400 Locked
else Invalid code
A->>DB: Increment attempts
A-->>C: 400 Invalid
else Valid code
A->>DB: Mark used, Activate User
A->>A: Generate Session & Tokens
A-->>C: 200 OK + Tokens + Profile + Session
end
2. Login, Sessions & OAuth
When a user logs in, a new Session is created or an existing one for the same device fingerprint is reused.
2.1 Standard Login (Password)
- Credentials Validation: Handled via Passport
LocalStrategy. - Error Privacy: All login failures return a generic structured 401 (
IDENTITY.INVALID_CREDENTIALS) so clients cannot infer whether email/password/account state caused the failure. - Session Limits: The system enforces a maximum of 10 concurrent sessions per user. If a new login exceeds this, the oldest active session is automatically revoked.
- Device Fingerprint: The user-agent is hashed to identify the device.
- Session Entity: Stores IP, browser, OS, and fingerprint.
- Token Generation: A new UUIDv7
jtiis created for the Refresh Token, which is tied to thesessionId. - 2FA Branching: If the user has active 2FA methods, the login response changes to
{ twoFactorToken, availableMethods }and the client must complete/api/identity/two-factor/verifybefore tokens are issued. - Response Payload: When 2FA is not required, login returns the token pair plus actor profile (
id,email,firstName,lastName, nullablephone), the current session entry (session), andorganizations: every organization where the user has a membership, ordered byorgId, each withorgId,type(AgencyorBusiness), nullablename, androleName(assigned role in that org). The client can use this list to choose an active org context (for examplex-org-idor agency switch) and to display the user’s role per org.
sequenceDiagram
participant C as Client
participant A as Auth API
participant DB as Database
C->>A: POST /api/identity/auth/user/login {email, password}
A->>A: Validate Credentials
A->>DB: Check active sessions count
opt Active sessions >= 10
A->>DB: Revoke oldest session
end
A->>DB: Create/Update Session (Fingerprint)
A->>A: Generate Token Pair (tied to Session ID)
A-->>C: 200 OK + Tokens
2.2 Google OAuth Flow
Google auth is now intent-driven. The client must first declare whether it wants to log in or create a specific kind of user account.
- The client starts with
POST /api/identity/auth/user/google/startand sends{ action }, whereactionis one of:loginregisterAgencyregisterBusinessregisterClient
registerAgencyis only valid withoutx-agency-idand means “create a brand-new agency owner account”.login,registerBusiness, andregisterClientare only valid withx-agency-idand are fully agency-scoped.- On the panel
/authroute (agency-owner entry,useAuthFlow), clicking Google on the email step opens an intent dialog: Iniciar sesión sendslogin; Crear agencia sendsregisterAgency. If the user is already on the password login or register step and Google were offered there, the panel would callgoogle/startwithloginorregisterAgencydirectly (same pattern as/business). - The API stores a one-time OAuth state in Redis and returns a Google redirect URL instead of encoding raw agency data in the browser-visible state.
- The callback consumes the one-time state and resolves the Google profile against the requested action:
login: find by(googleId, agencyId)first, then fallback to(email, agencyId)and link Google if needed. If no agency-scoped user exists, the callback fails. Login never creates a user implicitly.registerAgency: create a new agency org, a new user row scoped to that new agency, the owner membership, and the owner system role.registerBusiness: inside the provided agency, create a new user row plus a new business org and owner membership.registerClient: inside the provided agency, create a new user row plus a client membership in the agency.
- The same Google account can be used in multiple agencies. The persistence model now treats Google linkage as agency-scoped rather than globally unique.
- If active 2FA methods exist, the callback redirects back to the panel auth route with
{ twoFactorToken, availableMethods }encoded in query params instead of returning the final session payload immediately. - Otherwise the callback redirects back to the panel auth route with tokens and
organizations, and the panel completes sign-in from those redirect query params.
2.3 2FA Verification Hand-off
twoFactorTokenis a dedicated 5-minute JWT withpurpose: 'two-factor'.availableMethodslists the user's currently active methods (EMAIL,TOTP).- For
EMAIL, the client sendsPOST /api/identity/two-factor/email/send-codewith{ purpose: 'LOGIN' }. - The client then submits
POST /api/identity/two-factor/verifywith{ method, code }and the temporary Bearer token. - On success, the API returns the same final login payload as a normal login (including
organizationsas above).
3. Access & Refresh Token Usage
Tokens govern access to protected endpoints.
3.1 Access Token Validation
Every protected request checks the Access Token.
- The token signature and expiration are verified.
- Session Validation: The
sessionIdinside the token is checked against the database. Ifsession.isActiveis false, the request is immediately rejected (401 Unauthorized), even if the JWT itself hasn't expired. - Error Privacy: Token/session validation failures use a generic structured 401 (
IDENTITY.UNAUTHORIZED) without exposing payload/session internals.
3.2 Refresh Token Rotation & Reuse Detection
When the Access Token expires, the client uses the Refresh Token to get a new pair.
- The Refresh Token is verified (signature, expiration, and session active status).
- Atomic Rotation: The system atomically marks the refresh token hash as revoked in the database.
- Reuse Detection: If the database reports the token was already revoked, a token theft event is assumed. The system instantly revokes ALL sessions for that user, logs a warning, and returns
IDENTITY.REFRESH_TOKEN_REUSE_DETECTED. - On success, a new Token Pair is returned, preserving the same
sessionIdfrom the rotated token.
sequenceDiagram
participant C as Client
participant A as Auth API
participant DB as Database
C->>A: POST /api/identity/auth/refresh {refreshToken}
A->>A: Verify JWT & extract Session ID
A->>DB: Check if Session is Active
A->>DB: Atomic Update: Revoke Token Hash
alt Was already revoked? (Reuse Detected)
A->>DB: Revoke ALL User Sessions
A-->>C: 401 Unauthorized
else Valid Rotation
A->>A: Generate New Token Pair
A->>DB: Store New Token Hash
A-->>C: 200 OK + New Tokens
end
4. Profile Updates, Logout, Revocation & Agency Switching
Security settings invalidations affect tokens differently.
- Current Profile Read: Authenticated users can call
GET /api/identity/user/currentto retrieve the current user profile from the active access token claims without changing tokens or session state. - Profile Details Update: Authenticated users can call
PUT /api/identity/userwith{ firstName, lastName, phone, contactChannels }to update their profile without rotating tokens or affecting any active session. phoneis the nullable default phone number for the user profile and is expected in E.164 format when present.contactChannelsis a nullablejsonb-backed list of up to 20 labeled channels shaped as{ type, label, value }, wheretypeisEMAILorPHONE.- Explicit Logout: The client calls
POST /api/identity/auth/logoutwith a valid access token. The current authenticated session and all refresh tokens linked to it are revoked. - Password Change:
POST /api/identity/auth/user/change-passwordis protected by@RequiresTwoFactor(CHANGE_PASSWORD)when the user has active 2FA methods. Email-based proof usesPOST /api/identity/two-factor/challenge/send-email-codeplusx-two-factor-methodandx-two-factor-codeheaders on the password-change request. After success, all other sessions are revoked, current-session refresh tokens are revoked, and a fresh token pair is returned for the same current session. - Switch Agency: When a user switches their active agency context (
POST /api/identity/auth/user/switch-agency), only the current session rotates refresh tokens and receives new claims with the selectedagencyId. Other device sessions remain intact.
sequenceDiagram
participant C as Client
participant A as Auth API
participant DB as Database
C->>A: POST /api/identity/auth/user/change-password
A->>DB: Update Password Hash
A->>DB: Revoke ALL Sessions (except current)
A->>DB: Revoke current-session refresh tokens
A->>A: Generate fresh token pair for current session
A-->>C: 200 OK + Tokens
5. Route Index
| Method | Path | Purpose |
|---|---|---|
POST | /api/identity/auth/user/check-email | Pre-auth email discovery |
POST | /api/identity/auth/user/register | Start user registration |
POST | /api/identity/auth/user/verify-email | Complete email verification and create first session |
POST | /api/identity/auth/user/login | Password login |
POST | /api/identity/auth/user/google/start | Start Google OAuth with explicit login/register intent |
GET | /api/identity/auth/user/google | Start Google OAuth |
GET | /api/identity/auth/user/google/callback | Complete Google OAuth and redirect back to panel auth |
POST | /api/identity/auth/refresh | Refresh token rotation |
POST | /api/identity/auth/logout | Revoke current session |
POST | /api/identity/auth/user/change-password | Change password and rotate current session tokens |
POST | /api/identity/auth/user/switch-agency | Rotate current session into another owned agency |
GET | /api/identity/user/current | Read current user profile |
PUT | /api/identity/user | Update current user profile |
GET | /api/identity/agency/current | Read current agency profile |
POST | /api/identity/agency | Create an owned agency |
PUT | /api/identity/agency/:id | Update an owned agency |