Business DocumentationProgressive Auth Flow

Progressive Authentication Flow

Overview

The LandlordX authentication system implements a progressive multi-step flow that guides users through onboarding based on their completion status. The system supports two authentication methods for existing users: password login and OTP login. New users must complete OTP verification followed by profile creation.

Authentication Architecture

API Endpoints

EndpointMethodAuth RequiredDescription
/auth/send-otpPOSTNoSend OTP to phone number, returns user status
/auth/verifyPOSTNoVerify OTP and authenticate user
/auth/loginPOSTNoLogin with phone number and password
/auth/logoutPOSTYes (JWT)Logout and invalidate session

Database Entities

┌─────────────────────────────────────────────────────────────────┐
│                         users                                    │
├─────────────────────────────────────────────────────────────────┤
│ id (PK)           │ Auto-increment integer                      │
│ user_id           │ UUID (unique)                               │
│ phone             │ varchar(20)                                 │
│ first_name        │ text                                        │
│ last_name         │ varchar(255), nullable                      │
│ email             │ varchar(255), nullable                      │
│ password          │ varchar(255), nullable - hashed password    │
│ last_login_at     │ timestamp, nullable - updated on each login │
│ role              │ enum (LANDLORD, TENANT, ADMIN)              │
│ kyc_verified      │ boolean (default: false)                    │
│ created_at        │ timestamp                                   │
│ updated_at        │ timestamp                                   │
│ is_active         │ boolean (default: true)                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    user_otp_sessions                             │
├─────────────────────────────────────────────────────────────────┤
│ id (PK)           │ Auto-increment integer                      │
│ phone_number      │ varchar(20), unique, indexed                │
│ otp               │ varchar(6) - 6-digit OTP                    │
│ expire_at         │ timestamp - 5 minute expiry                 │
│ created_at        │ timestamp                                   │
│ updated_at        │ timestamp                                   │
│ is_active         │ boolean (default: true)                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                   user_auth_sessions                             │
├─────────────────────────────────────────────────────────────────┤
│ id (PK)           │ Auto-increment integer                      │
│ user_id (FK)      │ Foreign key to users.id                     │
│ auth_token        │ varchar(500) - JWT token                    │
│ expires_by        │ timestamp - 1 hour expiry                   │
│ created_at        │ timestamp                                   │
│ updated_at        │ timestamp                                   │
│ is_active         │ boolean (default: true)                     │
└─────────────────────────────────────────────────────────────────┘

Authentication Flows

Flow Overview Diagram

New User Onboarding Journey

┌─────────────────────────────────────────────────────────────────┐
│                  New User Onboarding Flow                       │
└─────────────────────────────────────────────────────────────────┘

Step 1: Request OTP

   ├─> User enters phone number
   ├─> API: POST /auth { phone_number }
   ├─> Response: { user: null, message: "OTP sent" }


Step 2: Verify OTP

   ├─> User receives OTP via WhatsApp
   ├─> User enters 6-digit OTP
   ├─> API: POST /auth/verify { phone_number, otp }
   ├─> Backend creates new user with role=LANDLORD
   ├─> Response: { access_token, user, status: "NEEDS_PROFILE_COMPLETION" }


Step 3: Create Profile

   ├─> User enters first_name, last_name, password
   ├─> Password is hashed on frontend before sending
   ├─> Profile saved to users table
   ├─> has_completed_profile becomes true in response


   ┌───────────────────────────────┐
   │ Check: has_verified_profile?  │
   └───────────────────────────────┘

           ├─ NO ──> Step 4: Iniitate eKYC Process
           │            │
           │            ├─> User connects DigiLocker
           │            ├─> Set has_verified_profile = true (user_kyc has status = 'success')
           │            │
           │            ▼
           ├─ YES ──> Dashboard (All steps complete)

Existing User Login Journey

┌─────────────────────────────────────────────────────────────────┐
│                 Existing User Login Flow                         │
└─────────────────────────────────────────────────────────────────┘

Step 1: Identify User

   ├─> User enters phone number
   ├─> API: POST /auth { phone_number }
   ├─> Response: {
   │       message: "OTP sent",
   │       user: { first_name, last_name, email, phone_number }
   │   }


Step 2: Choose Login Method

   ├─────────────────────────────────────────────┐
   │                                             │
   ▼                                             ▼
Option A: Password Login                 Option B: OTP Login
   │                                             │
   ├─> User enters password                      ├─> API: POST /auth/send-otp { phone_number }
   ├─>  API: POST /auth/login                    ├─> User receives OTP via WhatsApp
   │    { phone_number, password }               ├─> User enters 6-digit OTP
   │                                             ├─> API: POST /auth/verify
   │                                             │       { phone_number, otp }
   │                                             │
   ├─> Verify hashed password                    ├─> Verify OTP against database
   ├─> Create session in user_auth_sessions      ├─> Delete OTP record after use
   ├─> Update last_login_at                      ├─> Update last_login_at
   │                                             │
   └─────────────────┬───────────────────────────┘


              Return JWT Token


           Route based on user status

Logout Flow

┌─────────────────────────────────────────────────────────────────┐
│                        Logout Flow                               │
└─────────────────────────────────────────────────────────────────┘

   ├─> API: POST /auth/logout (with JWT in Authorization header)
   ├─> Backend validates JWT token
   ├─> Find active session in user_auth_sessions by user_id
   ├─> Delete session record
   ├─> Response: { message: "Logged out successfully" }

Sequence Diagrams

New User Onboarding Sequence

Existing User Password Login Sequence

Logout Sequence

User States

State 1: Unauthenticated

  • Flags: isAuthenticated = false, user = null
  • Access: /auth/login (phone number entry), /auth/verify (OTP verification)
  • Blocked: All other routes
  • Next Step: Login via OTP or Password

State 2: Authenticated, No Profile

  • Flags: isAuthenticated = true, has_completed_profile = false
  • Backend Status: NEEDS_PROFILE_COMPLETION
  • Access: /auth/create-profile
  • Blocked: /auth/login, /auth/send-otp, /auth/verify, /auth/digilocker, /dashboard
  • Next Step: Complete profile (first_name, last_name, email, password)

State 3: Authenticated, Profile Complete, No eKYC

  • Flags: isAuthenticated = true, has_completed_profile = true, has_verified_profile = false
  • Backend Status: NEEDS_VERIFICATION
  • Access: /auth/digilocker
  • Blocked: /auth/login, /auth/send-otp, /auth/verify, /auth/create-profile, /dashboard
  • Next Step: Complete eKYC via DigiLocker

State 4: Fully Onboarded

  • Flags: isAuthenticated = true, has_completed_profile = true, has_verified_profile = true
  • Backend Status: LOGIN_SUCCESSFUL
  • Access: /dashboard and all protected routes
  • Blocked: All /auth/* routes (redirected to dashboard)
  • Next Step: Dashboard and application features

Route Protection Rules

Login & Verify (/auth/login, /auth/verify)

  • Allow: Unauthenticated users
  • Redirect: Authenticated users → Next incomplete step

Create Profile (/auth/create-profile)

  • Allow: Authenticated users with hasCompletedProfile = false
  • Redirect: Unauthenticated users → /auth/login
  • Redirect: Users with profile complete → Next step (DigiLocker or Dashboard)

DigiLocker (/auth/digilocker)

  • Allow: Authenticated users with hasCompletedProfile = true AND hasVerifiedProfile = false
  • Redirect: Unauthenticated users → /auth/login
  • Redirect: Users without profile → /auth/create-profile
  • Redirect: Users with DigiLocker connected → /dashboard

Protected Routes (/dashboard, etc.)

  • Allow: Fully onboarded users (all flags true)
  • Redirect: Unauthenticated users → /auth/login
  • Redirect: Users with incomplete onboarding → Next required step

Implementation

API Response Types

Send OTP Response:

interface SendOtpResponse {
  message: string;
  user?: {
    user_id: string; // UUID
    first_name: string;
    last_name?: string;
    email?: string;
    phone_number: string;
  };
}

Auth Response (Login/Verify):

interface AuthResponse {
  access_token: string; // JWT token with 7-day expiry
  user: {
    id: number;
    phone: string;
    firstName?: string;
    lastName?: string;
    email?: string;
    role: UserRole; // "landlord" | "tenant" | "manager"
    has_completed_profile: boolean; // true when first_name, last_name, email are set
    has_verified_profile: boolean; // true when user_kyc.status = "success"
  };
  status: AuthStatus; // "LOGIN_SUCCESSFUL" | "NEEDS_PROFILE_COMPLETION" | "NEEDS_VERIFICATION"
}

User Interface (Frontend)

export interface User {
  id: number;
  phone: string;
  firstName?: string; // Mapped from backend's first_name
  lastName?: string; // Mapped from backend's last_name
  email?: string;
  role: UserRole; // "landlord" | "tenant" | "manager"
  hasCompletedProfile: boolean; // Mapped from backend's has_completed_profile
  hasVerifiedProfile: boolean; // Mapped from backend's has_verified_profile
}

Profile Completion Logic

The backend determines has_completed_profile based on:

// From apps/api-app/src/modules/auth/auth.service.ts
function hasCompletedProfile(user: User): boolean {
  return !!(user.first_name && user.last_name && user.email);
}
 
function hasVerifiedProfile(user: User): boolean {
  // Check if user has kyc_requests with at least one successful KYC
  const kycRequests = user.kyc_requests;
  if (!kycRequests || !Array.isArray(kycRequests)) {
    return false;
  }
  return kycRequests.some((req) => req.status === UserKycStatus.SUCCESS);
}

Backend UserKycStatus Enum:

  • CREATED: Link created but not used
  • KYC_REQUESTED: Consent success and KYC request success from NPCI
  • SUCCESS: Received KYC data from NPCI (profile verified)
  • ERROR: Any error during KYC process

Auth Flow Utilities

File: src/lib/auth/auth-flow.ts

// Determines the next required step
getNextAuthStep(user: User | null): string
 
// Checks if user can access a specific step
canAccessAuthStep(user: User | null, requestedStep: string): boolean

Auth Guard

File: src/components/guards/auth-guard.tsx

Automatically:

  • Checks user state on route access
  • Redirects to appropriate next step
  • Prevents skipping steps
  • Handles loading states

Page Updates

Verify Page: Sets user after OTP verification, relies on AuthGuard for routing

Create Profile Page: Updates hasCompletedProfile = true after form submission

DigiLocker Page: Updates hasVerifiedProfile = true after connection (or skip)

Example User Journeys

Journey 1: New User (OTP Onboarding)

  1. User visits /dashboard → Redirected to /auth/login
  2. User enters phone number
  3. API returns is_existing_user: false
  4. User receives OTP via WhatsApp
  5. User enters OTP → Backend creates user, returns status: "NEEDS_PROFILE_COMPLETION"
  6. AuthGuard redirects to /auth/create-profile
  7. User fills profile (first_name, last_name, password) → hasCompletedProfile = true
  8. AuthGuard redirects to /auth/digilocker
  9. User connects DigiLocker → hasVerifiedProfile = true
  10. AuthGuard redirects to /dashboard

Journey 2: Returning User (Password Login)

  1. User visits /auth/login
  2. User enters phone number
  3. API returns is_existing_user: true with user info
  4. User chooses password login option
  5. User enters password
  6. API validates credentials, creates session in user_auth_sessions
  7. Returns status: "LOGIN_SUCCESSFUL"
  8. AuthGuard redirects to /dashboard

Journey 3: Returning User (OTP Login)

  1. User visits /auth/login
  2. User enters phone number
  3. API returns is_existing_user: true with user info
  4. OTP sent via WhatsApp
  5. User chooses OTP login option
  6. User enters OTP
  7. API verifies OTP
  8. Returns status: "LOGIN_SUCCESSFUL"
  9. AuthGuard redirects to /dashboard

Journey 4: User Tries to Skip Steps

  1. User completes OTP verification
  2. User manually navigates to /auth/digilocker
  3. AuthGuard checks: hasCompletedProfile = false
  4. AuthGuard redirects to /auth/create-profile
  5. User must complete profile before accessing DigiLocker

Journey 5: Session Logout

  1. User clicks logout button
  2. Frontend calls POST /auth/logout with JWT
  3. Backend deletes session from user_auth_sessions
  4. Frontend clears stored token
  5. User redirected to /auth/login

Testing Scenarios

Scenario 1: Skip Profile Creation

  • Navigate to /auth/digilocker without completing profile
  • Expected: Redirect to /auth/create-profile

Scenario 2: Skip DigiLocker

  • Complete profile, try to access /dashboard
  • Expected: Redirect to /auth/digilocker

Scenario 3: Access Auth Routes When Logged In

  • Login with all steps complete
  • Try to access /auth/login
  • Expected: Redirect to /dashboard

Scenario 4: Direct URL Access

  • Copy URL /auth/create-profile without authentication
  • Expected: Redirect to /auth/login

Scenario 5: Page Reload Mid-Flow

  • Complete profile but not DigiLocker
  • Reload page
  • Expected: Stay on or return to /auth/digilocker

Production Considerations

Backend Implementation Status

The following backend features are implemented:

  1. OTP Generation & Delivery: 6-digit OTP via WhatsApp with 5-minute expiry
  2. OTP Verification: Validates OTP, creates user if new, returns JWT token
  3. Password Login: Validates hashed password, creates session in user_auth_sessions
  4. Session Management: JWT tokens with 7-day expiry, sessions with 1-hour expiry
  5. Logout: Deletes session from user_auth_sessions
  6. User Status Detection: is_existing_user flag to differentiate login vs signup

Security Architecture

  • Password Hashing: Passwords are hashed on the frontend before transmission
  • JWT Authentication: Tokens include user ID and phone number in payload
  • Session Tracking: Active sessions stored in user_auth_sessions for audit
  • OTP Consumption: OTP records are deleted after successful verification
  • Guard Protection: JwtAuthGuard protects authenticated endpoints

Session Expiry

Session TypeExpiry DurationStorageConfiguration Constant
OTP5 minutesuser_otp_sessionsOTP_EXPIRY_MINUTES = 5
Auth Session1 hour (for session tracking only)user_auth_sessionsSESSION_EXPIRY_HOURS = 1
JWT Token7 days (actual auth validity)Client-side (localStorage)Hardcoded in jwtService.sign()

Optional Features

  • Skip DigiLocker Permanently: Add hasSkippedDigiLocker flag if users can permanently skip
  • Edit Profile Later: Allow users to update profile from settings even after completion
  • Multi-step Forms: Break profile creation into multiple sub-steps if needed
  • Progress Indicator: Show visual progress (1/4, 2/4, etc.) in auth flow
  • Remember Me: Extend JWT expiry for trusted devices

Security Notes

  • All auth routes are client-side protected via AuthGuard
  • Backend validates JWT on every protected API call
  • Don’t rely solely on client-side flags for authorization
  • Deactivate old sessions when creating new ones (single session per user)
  • OTP brute-force protection should be implemented (rate limiting)