Kulcare Docs
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

MethodDescriptionUse Case
TOTPTime-based One-Time Password (Google Authenticator, Authy, etc.)Most secure option
SMS6-digit code sent via text messageConvenient, no app needed
Email6-digit code sent to registered emailFallback option
Recovery Codes10 one-time-use backup codesEmergency access

API Endpoints

All 2FA endpoints are part of the new Identity domain (/identity/*).

#EndpointMethodPurpose
1/identity/sessionsPOSTInitial login - checks 2FA requirement
2/identity/two_factor/verifyPOSTVerify code during login
3/identity/two_factor/statusGETGet current 2FA status
4/identity/two_factor/enablePOSTStart 2FA setup
5/identity/two_factor/confirmPOSTConfirm setup with verification code
6/identity/two_factor/disablePOSTDisable 2FA
7/identity/two_factor/regenerate_recovery_codesPOSTGenerate 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

ServiceFilePurpose
EnableServiceservices/two_factor/enable_service.rbStart 2FA setup, generate QR code/code
ConfirmServiceservices/two_factor/confirm_service.rbVerify code, create 2FA setting
VerifyServiceservices/two_factor/verify_service.rbVerify code during login
DisableServiceservices/two_factor/disable_service.rbDisable 2FA
StatusServiceservices/two_factor/status_service.rbGet 2FA status
RegenerateRecoveryServiceservices/two_factor/regenerate_recovery_service.rbGenerate new codes
CheckTwoFactorServiceservices/sessions/check_two_factor_service.rbLogin 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
end

Database Tables (identity schema)

identity.two_factor_settings

Stores the main 2FA configuration for each user.

ColumnTypeDescription
iduuidPrimary key (UUIDv7)
user_iduuidForeign key to identity.users
methodstring'TOTP', 'SMS', or 'EMAIL'
enabledbooleanWhether 2FA is active
totp_secretstringEncrypted TOTP secret
enabled_atdatetimeWhen 2FA was enabled
last_verified_atdatetimeLast successful verification
failed_attemptsintegerFailed attempt count
locked_untildatetimeLockout expiration

identity.two_factor_recovery_codes

Stores hashed recovery codes for backup access.

ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to identity.users
code_hashstringSHA256 hash of recovery code
used_atdatetimeWhen code was used
created_atdatetimeCreation timestamp

identity.two_factor_metrics

Audit log for all 2FA events.

ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to identity.users
event_typestring'enable_attempt', 'verification_success', etc.
methodstring'TOTP', 'SMS', 'EMAIL'
device_uuidstringDevice identifier
ip_addressinetUser's IP address
successbooleanWhether operation succeeded
created_atdatetimeEvent timestamp

identity.devices

Trusted device management (separate from 2FA but related).

ColumnTypeDescription
iduuidPrimary key
user_iduuidForeign key to identity.users
device_typestring'web', 'ios', 'android'
device_uuidstringUnique device identifier
device_namestringUser-friendly name
trusted_atdatetimeWhen device was trusted
last_trusted_ipstringIP when last trusted

Frontend Implementation

Shared Services (API Functions)

File: packages/shared-services/src/domains/identity/api.ts

FunctionEndpointPurpose
getTwoFactorStatus()GET /identity/two_factor/statusGet 2FA status
enableTwoFactor(params)POST /identity/two_factor/enableStart setup
confirmTwoFactor(params)POST /identity/two_factor/confirmConfirm setup
verifyTwoFactor(params)POST /identity/two_factor/verifyVerify code
disableTwoFactor(password)POST /identity/two_factor/disableDisable 2FA
regenerateRecoveryCodes()POST /identity/two_factor/regenerate_recovery_codesNew codes

React Query Hooks

File: packages/shared-services/src/domains/identity/hooks.ts

HookPurpose
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

ComponentFilePurpose
TwoFactorAuthPageapps/shell/src/pages/settings/TwoFactorAuthPage.tsxMain 2FA management page
TwoFactorSetupapps/shell/src/components/settings/TwoFactorSetup.tsx2FA setup flow
RecoveryCodeManagementapps/shell/src/components/settings/RecoveryCodeManagement.tsxView/regenerate codes
DisableTwoFactorModalapps/shell/src/components/settings/DisableTwoFactorModal.tsxDisable confirmation
TwoFactorVerificationapps/shell/src/components/auth/TwoFactorVerification.tsxCode entry during login
DeviceManagementapps/shell/src/components/settings/DeviceManagement.tsxTrusted devices (legacy API)

Security Features

Rate Limiting

EndpointLimit
Enable3 attempts per hour per user
Verify5 attempts per minute per user
Account lockout15 minutes after 5 failed attempts

Data Protection

FeatureImplementation
TOTP secretsEncrypted via Rails 7 encrypts API
Recovery codesSHA256 hashed before storage
Verification codesCached with 5-minute expiration
Device trust30-day expiration

Audit Trail

All 2FA events are logged to identity.two_factor_metrics:

  • enable_attempt - 2FA setup initiated
  • confirmation_success / confirmation_failure - Setup confirmation
  • verification_success / verification_failure - Login verification
  • code_sent - SMS/Email code sent
  • disable_attempt - 2FA disable attempted


Implementation Status

ComponentStatus
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

On this page