General
Two-Factor Authentication (2FA) Workflow
Status: Complete Date: 2026-03-19
Overview
This document describes the complete Two-Factor Authentication (2FA) implementation in the Kulcare platform. The 2FA system provides an extra layer of security by requiring a second form of authentication beyond the password.
Supported Methods
| Method | Description | Use Case |
|---|---|---|
| TOTP | Time-based One-Time Password (Google Authenticator, Authy, etc.) | Most secure option |
| SMS | 6-digit code sent via text message | Convenient, no app needed |
| 6-digit code sent to registered email | Fallback option | |
| Recovery Codes | 10 one-time-use backup codes | Emergency access |
API Endpoints
All 2FA endpoints are part of the new Identity domain (/identity/*).
| # | Endpoint | Method | Purpose |
|---|---|---|---|
| 1 | /identity/sessions | POST | Initial login - checks 2FA requirement |
| 2 | /identity/two_factor/verify | POST | Verify code during login |
| 3 | /identity/two_factor/status | GET | Get current 2FA status |
| 4 | /identity/two_factor/enable | POST | Start 2FA setup |
| 5 | /identity/two_factor/confirm | POST | Confirm setup with verification code |
| 6 | /identity/two_factor/disable | POST | Disable 2FA |
| 7 | /identity/two_factor/regenerate_recovery_codes | POST | Generate new recovery codes |
Complete Workflow Diagrams
Login Flow with 2FA
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2FA LOGIN WORKFLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐
│ User Login │
│ (Credentials) │
└──────┬───────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ POST /identity/sessions │
│ Request: {phone, country_code, password, device_details} │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend: SessionsController#create → CheckTwoFactorService │
│ │
│ 1. Verify user credentials │
│ 2. Check user.two_factor_setting.enabled? │
│ 3. Check if device is trusted │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
2FA Disabled Device Trusted Device Not Trusted
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────────────┐
│ Return Tokens │ │ Return Tokens │ │ Return 2FA Required Response │
│ (Complete) │ │ (Skip 2FA) │ │ { │
└─────────────────┘ └─────────────────┘ │ requires_2fa: true, │
│ two_factor_method: 'SMS', │
│ temp_auth_token, │
│ masked_contact: '***1234' │
│ } │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Frontend: TwoFactorVerification │
│ - User enters verification code │
│ - Selects method (if SMS/Email) │
│ - Optional: Trust device checkbox │
└────────────────────┬──────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ POST /identity/two_factor/verify │
│ Request: {code, method, trust_device} │
└────────────────────┬─────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ Backend: VerifyService │
│ 1. Validate input │
│ 2. Check 2FA enabled │
│ 3. Check account lock │
│ 4. Verify code (TOTP/SMS/Email/Recovery) │
│ 5. Complete verification │
│ 6. Trust device (if requested) │
│ 7. Generate tokens │
└────────────────────┬─────────────────────────┘
│
▼
┌────────────────────────────────────────────────┐
│ Response: {user, auth_token, refresh_token} │
│ Login Complete │
└────────────────────────────────────────────────┘Enable 2FA Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ ENABLE 2FA WORKFLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ User goes to │
│ Security Settings │
└──────────┬───────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ GET /identity/two_factor/status │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Response: │
│ {enabled: false} │
└────────────┬───────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ User clicks "Enable 2FA" │
└──────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ POST /identity/two_factor/enable │
│ Request: {method: 'TOTP' | 'SMS' | 'EMAIL'} │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend: EnableService │
│ │
│ If TOTP: │
│ 1. Generate TOTP secret │
│ 2. Generate QR code (qr_code_svg) │
│ 3. Create provisioning_uri │
│ 4. Cache setup data (5 min expiry) │
│ │
│ If SMS/Email: │
│ 1. Generate 6-digit code │
│ 2. Send via SMS job / Email │
│ 3. Cache code (10 min expiry) │
│ 4. Return masked_contact │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Response: │
│ TOTP: {qr_code_svg, provisioning_uri} │
│ SMS/Email: {masked_contact: '*******1234'} │
└──────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Frontend: TwoFactorSetup │
│ │
│ TOTP Flow: │
│ - Display QR code │
│ - User scans with authenticator app │
│ - Enter code to verify │
│ │
│ SMS/Email Flow: │
│ - Display masked contact │
│ - Code sent automatically │
│ - Enter code to verify │
└────────────────────┬────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ POST /identity/two_factor/confirm │
│ Request: {code: '123456'} │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend: ConfirmService │
│ │
│ 1. Verify code (TOTP: against secret, SMS/Email: against cache) │
│ 2. Create TwoFactorSetting record │
│ 3. Generate 10 recovery codes │
│ 4. Clear setup cache │
│ 5. Record metric │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Response: │
│ { │
│ recovery_codes: ['abc123', 'def456', │
│ 'ghi789', ...] │
│ } │
└────────────────────────────────────────────┘Disable 2FA Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ DISABLE 2FA WORKFLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ User goes to │
│ Security Settings │
└──────────┬───────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ GET /identity/two_factor/status │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Response: │
│ {enabled: true, method: │
│ 'TOTP'} │
└────────────┬───────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ User clicks "Disable 2FA" │
│ - Confirmation dialog │
│ - Enter password │
└──────────────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ POST /identity/two_factor/disable │
│ Request: {password: 'user_password'} │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend: DisableService │
│ │
│ 1. Validate password │
│ 2. Destroy TwoFactorSetting record │
│ 3. Delete all recovery codes │
│ 4. Record metric │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Response: │
│ {success: true, message: │
│ '2FA disabled │
│ successfully'} │
└────────────────────────────┘Regenerate Recovery Codes Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ REGENERATE RECOVERY CODES WORKFLOW │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ User goes to │
│ Security Settings │
│ → Recovery Codes │
└──────────┬───────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ POST /identity/two_factor/regenerate_recovery_codes │
│ (No request body needed - user is already authenticated) │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Backend: RegenerateRecoveryService │
│ │
│ 1. Check 2FA is enabled │
│ 2. Delete existing recovery codes │
│ 3. Generate 10 new recovery codes │
│ 4. Return new codes │
└──────────────────────────────┬──────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ Response: │
│ {recovery_codes: ['new1', │
│ 'new2', ...]} │
└────────────────────────────┘Backend Implementation
Services
| Service | File | Purpose |
|---|---|---|
EnableService | services/two_factor/enable_service.rb | Start 2FA setup, generate QR code/code |
ConfirmService | services/two_factor/confirm_service.rb | Verify code, create 2FA setting |
VerifyService | services/two_factor/verify_service.rb | Verify code during login |
DisableService | services/two_factor/disable_service.rb | Disable 2FA |
StatusService | services/two_factor/status_service.rb | Get 2FA status |
RegenerateRecoveryService | services/two_factor/regenerate_recovery_service.rb | Generate new codes |
CheckTwoFactorService | services/sessions/check_two_factor_service.rb | Login flow integration |
Controller
TwoFactorController (controllers/two_factor_controller.rb)
namespace :identity do
namespace :two_factor do
post :enable # Start 2FA setup
post :confirm # Confirm with code
post :verify # Verify during login
post :disable # Disable 2FA
get :status # Get status
post :regenerate_recovery_codes # New recovery codes
end
endDatabase Tables (identity schema)
identity.two_factor_settings
Stores the main 2FA configuration for each user.
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key (UUIDv7) |
| user_id | uuid | Foreign key to identity.users |
| method | string | 'TOTP', 'SMS', or 'EMAIL' |
| enabled | boolean | Whether 2FA is active |
| totp_secret | string | Encrypted TOTP secret |
| enabled_at | datetime | When 2FA was enabled |
| last_verified_at | datetime | Last successful verification |
| failed_attempts | integer | Failed attempt count |
| locked_until | datetime | Lockout expiration |
identity.two_factor_recovery_codes
Stores hashed recovery codes for backup access.
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| user_id | uuid | Foreign key to identity.users |
| code_hash | string | SHA256 hash of recovery code |
| used_at | datetime | When code was used |
| created_at | datetime | Creation timestamp |
identity.two_factor_metrics
Audit log for all 2FA events.
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| user_id | uuid | Foreign key to identity.users |
| event_type | string | 'enable_attempt', 'verification_success', etc. |
| method | string | 'TOTP', 'SMS', 'EMAIL' |
| device_uuid | string | Device identifier |
| ip_address | inet | User's IP address |
| success | boolean | Whether operation succeeded |
| created_at | datetime | Event timestamp |
identity.devices
Trusted device management (separate from 2FA but related).
| Column | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| user_id | uuid | Foreign key to identity.users |
| device_type | string | 'web', 'ios', 'android' |
| device_uuid | string | Unique device identifier |
| device_name | string | User-friendly name |
| trusted_at | datetime | When device was trusted |
| last_trusted_ip | string | IP when last trusted |
Frontend Implementation
Shared Services (API Functions)
File: packages/shared-services/src/domains/identity/api.ts
| Function | Endpoint | Purpose |
|---|---|---|
getTwoFactorStatus() | GET /identity/two_factor/status | Get 2FA status |
enableTwoFactor(params) | POST /identity/two_factor/enable | Start setup |
confirmTwoFactor(params) | POST /identity/two_factor/confirm | Confirm setup |
verifyTwoFactor(params) | POST /identity/two_factor/verify | Verify code |
disableTwoFactor(password) | POST /identity/two_factor/disable | Disable 2FA |
regenerateRecoveryCodes() | POST /identity/two_factor/regenerate_recovery_codes | New codes |
React Query Hooks
File: packages/shared-services/src/domains/identity/hooks.ts
| Hook | Purpose |
|---|---|
useTwoFactorStatus() | Get 2FA status |
useEnableTwoFactor() | Start 2FA setup |
useConfirmTwoFactor() | Confirm setup with code |
useVerifyTwoFactor() | Verify code during login |
useDisableTwoFactor() | Disable 2FA |
useRegenerateRecoveryCodes() | Generate new recovery codes |
Components
| Component | File | Purpose |
|---|---|---|
| TwoFactorAuthPage | apps/shell/src/pages/settings/TwoFactorAuthPage.tsx | Main 2FA management page |
| TwoFactorSetup | apps/shell/src/components/settings/TwoFactorSetup.tsx | 2FA setup flow |
| RecoveryCodeManagement | apps/shell/src/components/settings/RecoveryCodeManagement.tsx | View/regenerate codes |
| DisableTwoFactorModal | apps/shell/src/components/settings/DisableTwoFactorModal.tsx | Disable confirmation |
| TwoFactorVerification | apps/shell/src/components/auth/TwoFactorVerification.tsx | Code entry during login |
| DeviceManagement | apps/shell/src/components/settings/DeviceManagement.tsx | Trusted devices (legacy API) |
Security Features
Rate Limiting
| Endpoint | Limit |
|---|---|
| Enable | 3 attempts per hour per user |
| Verify | 5 attempts per minute per user |
| Account lockout | 15 minutes after 5 failed attempts |
Data Protection
| Feature | Implementation |
|---|---|
| TOTP secrets | Encrypted via Rails 7 encrypts API |
| Recovery codes | SHA256 hashed before storage |
| Verification codes | Cached with 5-minute expiration |
| Device trust | 30-day expiration |
Audit Trail
All 2FA events are logged to identity.two_factor_metrics:
enable_attempt- 2FA setup initiatedconfirmation_success/confirmation_failure- Setup confirmationverification_success/verification_failure- Login verificationcode_sent- SMS/Email code sentdisable_attempt- 2FA disable attempted
Related Documents
- Auth & User Management Frontend Migration - Migration from legacy endpoints
- Two-Factor Authentication Plan - Original implementation plan
- Session Tracking Design - Session management design
Implementation Status
| Component | Status |
|---|---|
| Backend API endpoints | ✅ Complete |
| Backend services | ✅ Complete |
| Database tables | ✅ Complete |
| Frontend API functions | ✅ Complete |
| Frontend hooks | ✅ Complete |
| Login flow integration | ✅ Complete |
| Legacy code cleanup | ✅ Complete |