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
| Endpoint | Method | Auth Required | Description |
|---|---|---|---|
/auth/send-otp | POST | No | Send OTP to phone number, returns user status |
/auth/verify | POST | No | Verify OTP and authenticate user |
/auth/login | POST | No | Login with phone number and password |
/auth/logout | POST | Yes (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 statusLogout 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:
/dashboardand 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 = trueANDhasVerifiedProfile = 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 usedKYC_REQUESTED: Consent success and KYC request success from NPCISUCCESS: 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): booleanAuth 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)
- User visits
/dashboard→ Redirected to/auth/login - User enters phone number
- API returns
is_existing_user: false - User receives OTP via WhatsApp
- User enters OTP → Backend creates user, returns
status: "NEEDS_PROFILE_COMPLETION" - AuthGuard redirects to
/auth/create-profile - User fills profile (first_name, last_name, password) →
hasCompletedProfile = true - AuthGuard redirects to
/auth/digilocker - User connects DigiLocker →
hasVerifiedProfile = true - AuthGuard redirects to
/dashboard
Journey 2: Returning User (Password Login)
- User visits
/auth/login - User enters phone number
- API returns
is_existing_user: truewith user info - User chooses password login option
- User enters password
- API validates credentials, creates session in
user_auth_sessions - Returns
status: "LOGIN_SUCCESSFUL" - AuthGuard redirects to
/dashboard
Journey 3: Returning User (OTP Login)
- User visits
/auth/login - User enters phone number
- API returns
is_existing_user: truewith user info - OTP sent via WhatsApp
- User chooses OTP login option
- User enters OTP
- API verifies OTP
- Returns
status: "LOGIN_SUCCESSFUL" - AuthGuard redirects to
/dashboard
Journey 4: User Tries to Skip Steps
- User completes OTP verification
- User manually navigates to
/auth/digilocker - AuthGuard checks:
hasCompletedProfile = false - AuthGuard redirects to
/auth/create-profile - User must complete profile before accessing DigiLocker
Journey 5: Session Logout
- User clicks logout button
- Frontend calls
POST /auth/logoutwith JWT - Backend deletes session from
user_auth_sessions - Frontend clears stored token
- User redirected to
/auth/login
Testing Scenarios
Scenario 1: Skip Profile Creation
- Navigate to
/auth/digilockerwithout 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-profilewithout 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:
- ✅ OTP Generation & Delivery: 6-digit OTP via WhatsApp with 5-minute expiry
- ✅ OTP Verification: Validates OTP, creates user if new, returns JWT token
- ✅ Password Login: Validates hashed password, creates session in
user_auth_sessions - ✅ Session Management: JWT tokens with 7-day expiry, sessions with 1-hour expiry
- ✅ Logout: Deletes session from
user_auth_sessions - ✅ User Status Detection:
is_existing_userflag 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_sessionsfor audit - OTP Consumption: OTP records are deleted after successful verification
- Guard Protection:
JwtAuthGuardprotects authenticated endpoints
Session Expiry
| Session Type | Expiry Duration | Storage | Configuration Constant |
|---|---|---|---|
| OTP | 5 minutes | user_otp_sessions | OTP_EXPIRY_MINUTES = 5 |
| Auth Session | 1 hour (for session tracking only) | user_auth_sessions | SESSION_EXPIRY_HOURS = 1 |
| JWT Token | 7 days (actual auth validity) | Client-side (localStorage) | Hardcoded in jwtService.sign() |
Optional Features
- Skip DigiLocker Permanently: Add
hasSkippedDigiLockerflag 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)