Skip to content

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: true indicates a user account already exists for that email.
  • found: false indicates 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, lastName to POST /api/identity/auth/user/register.
  • The system checks for existing emails.
  • A new User entity is created with isActive: false.
  • Registration also creates a default owned agency for that user and the initial owner membership so the first authenticated session already has an agencyId context.
  • 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, attempts is 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, nullable phone), the current session entry (session), and organizations (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 jti is created for the Refresh Token, which is tied to the sessionId.
  • 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/verify before tokens are issued.
  • Response Payload: When 2FA is not required, login returns the token pair plus actor profile (id, email, firstName, lastName, nullable phone), the current session entry (session), and organizations: every organization where the user has a membership, ordered by orgId, each with orgId, type (Agency or Business), nullable name, and roleName (assigned role in that org). The client can use this list to choose an active org context (for example x-org-id or 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/start and sends { action }, where action is one of:
    • login
    • registerAgency
    • registerBusiness
    • registerClient
  • registerAgency is only valid without x-agency-id and means “create a brand-new agency owner account”.
  • login, registerBusiness, and registerClient are only valid with x-agency-id and are fully agency-scoped.
  • On the panel /auth route (agency-owner entry, useAuthFlow), clicking Google on the email step opens an intent dialog: Iniciar sesión sends login; Crear agencia sends registerAgency. If the user is already on the password login or register step and Google were offered there, the panel would call google/start with login or registerAgency directly (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

  • twoFactorToken is a dedicated 5-minute JWT with purpose: 'two-factor'.
  • availableMethods lists the user's currently active methods (EMAIL, TOTP).
  • For EMAIL, the client sends POST /api/identity/two-factor/email/send-code with { purpose: 'LOGIN' }.
  • The client then submits POST /api/identity/two-factor/verify with { method, code } and the temporary Bearer token.
  • On success, the API returns the same final login payload as a normal login (including organizations as 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 sessionId inside the token is checked against the database. If session.isActive is 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 sessionId from 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/current to 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/user with { firstName, lastName, phone, contactChannels } to update their profile without rotating tokens or affecting any active session.
  • phone is the nullable default phone number for the user profile and is expected in E.164 format when present.
  • contactChannels is a nullable jsonb-backed list of up to 20 labeled channels shaped as { type, label, value }, where type is EMAIL or PHONE.
  • Explicit Logout: The client calls POST /api/identity/auth/logout with 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-password is protected by @RequiresTwoFactor(CHANGE_PASSWORD) when the user has active 2FA methods. Email-based proof uses POST /api/identity/two-factor/challenge/send-email-code plus x-two-factor-method and x-two-factor-code headers 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 selected agencyId. 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

MethodPathPurpose
POST/api/identity/auth/user/check-emailPre-auth email discovery
POST/api/identity/auth/user/registerStart user registration
POST/api/identity/auth/user/verify-emailComplete email verification and create first session
POST/api/identity/auth/user/loginPassword login
POST/api/identity/auth/user/google/startStart Google OAuth with explicit login/register intent
GET/api/identity/auth/user/googleStart Google OAuth
GET/api/identity/auth/user/google/callbackComplete Google OAuth and redirect back to panel auth
POST/api/identity/auth/refreshRefresh token rotation
POST/api/identity/auth/logoutRevoke current session
POST/api/identity/auth/user/change-passwordChange password and rotate current session tokens
POST/api/identity/auth/user/switch-agencyRotate current session into another owned agency
GET/api/identity/user/currentRead current user profile
PUT/api/identity/userUpdate current user profile
GET/api/identity/agency/currentRead current agency profile
POST/api/identity/agencyCreate an owned agency
PUT/api/identity/agency/:idUpdate an owned agency