XHealtXperience enforces two-factor authentication (2FA) as a first-class security control. TheDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Arthurr23/XHealtXperience/llms.txt
Use this file to discover all available pages before exploring further.
EnsureTwoFactorAuthenticated middleware (registered as two_factor) is applied to every protected route — including tenant clinic routes and the Super Admin panel. Once a user enables 2FA, they must successfully verify their identity on every new login session before accessing any protected resource.
Two authentication methods are supported: a TOTP authenticator app (powered by pragmarx/google2fa-laravel) and an Email OTP delivering a 6-digit code directly to the user’s registered email address.
User Model Fields
The following fields on theUser model track the full lifecycle of a user’s 2FA configuration:
| Field | Type | Purpose |
|---|---|---|
two_factor_secret | string|null | Encrypted TOTP secret key. null when using email-only method. |
two_factor_confirmed_at | datetime|null | Timestamp of the last successful confirmation. null means 2FA is not active. |
two_factor_method | string | Active method: 'app' (TOTP) or 'email'. Defaults to 'app'. |
two_factor_email_code | string|null | Encrypted current email OTP code. Never exposed in API responses. |
two_factor_email_expires_at | datetime|null | Expiry timestamp for the current email code (valid for 5 minutes). |
Supported Methods
TOTP — Authenticator App
The TOTP method usespragmarx/google2fa-laravel to generate time-based one-time passwords compatible with any RFC 6238 app, including Google Authenticator, Authy, and 1Password. A unique secret key is generated per user, encrypted at rest in the database, and rendered as a QR code during setup.
The QR code label is formatted as {User Name} ({Role}) · {Clinic Name} so it is immediately identifiable in an authenticator app’s list, even if the app truncates the label.
Email OTP
The email method sends a 6-digit code to the user’s registered email address. The code is generated withrandom_int, stored encrypted in two_factor_email_code, and expires after 5 minutes. Codes are single-use: a successful verification wipes two_factor_email_code and two_factor_email_expires_at from the database immediately.
When a user with the email method arrives at the 2FA challenge screen after login, a fresh code is sent automatically — but only if no valid unexpired code already exists. This prevents a flood of emails if the user refreshes the challenge page.
Setting Up 2FA — TOTP (App) Flow
Navigate to the 2FA setup page
The user opens their profile and navigates to the 2FA configuration screen. The frontend calls:The response includes
twoFactorEnabled, twoFactorPending, twoFactorMethod, and otpauthUrl (the QR code URL, or null if no secret exists yet).Generate the TOTP secret
The user selects the Authenticator App method and submits the enable form. The server generates a secure random secret key, encrypts it, and saves it to The page reloads with a QR code rendered from
two_factor_secret. The method is set to 'app' and two_factor_confirmed_at is cleared (pending state).otpauthUrl. The user scans this code with their authenticator app.Scan the QR code
The user scans the displayed QR code using Google Authenticator or any compatible TOTP app. The app starts generating 6-digit codes that rotate every 30 seconds.
Confirm the first code
The user enters the 6-digit code from their app to verify the secret was imported correctly. The server validates the code using On success,
Google2FA::verifyKey() with a 1-window drift tolerance.two_factor_confirmed_at is set to the current timestamp and the session receives auth.two_factor_verified = true. A two_factor.enabled event is written to the audit log.Setting Up 2FA — Email OTP Flow
Select the email method
The user navigates to the 2FA setup page and selects Email. The frontend submits:The server sets
two_factor_method = 'email' and clears two_factor_confirmed_at. A confirmation code is sent immediately to the user’s registered email address.Confirm the emailed code
The user enters the 6-digit code they received by email:The server decrypts the stored code, checks it against the input using constant-time comparison (
hash_equals), and verifies it has not expired. On success, two_factor_confirmed_at is set and the audit log records two_factor.enabled.The Challenge Flow (Post-Login Verification)
After a successful login, iftwo_factor_confirmed_at is not null, the user has not yet passed the 2FA challenge for this session. Any attempt to visit a protected route triggers the two_factor middleware, which:
- Saves the originally-intended URL to
session('url.intended'). - Redirects the user to the challenge screen.
email, a code is dispatched automatically when they land on the challenge page (provided no valid code exists already). The user can also request a fresh code on demand via:
Disabling 2FA
A user can disable 2FA from their profile settings. The request requires their current password as confirmation:two_factor.disabled is written to the audit log.
Changing the Active Method
Users with an active 2FA setup can switch betweenapp and email. Switching always clears two_factor_confirmed_at, requiring re-confirmation with a code from the new method before it becomes active:
Cancelling a Pending Setup
If a user starts the setup flow but does not complete the confirmation step, they can cancel and return to the unenrolled state:two_factor_confirmed_at is still null (i.e., the setup is genuinely incomplete). If 2FA is already active, this endpoint is a no-op.
Full Route Reference
The following table lists all 2FA-related routes. Tenant routes are prefixed with/{tenant}. The equivalent Super Admin (central) routes are prefixed with /panel-global.
| Method | Tenant Path | Central Path | Handler | Description |
|---|---|---|---|---|
GET | /two-factor/challenge | /two-factor/challenge | showChallenge | Render the post-login code challenge page |
POST | /two-factor/challenge | /two-factor/challenge | verify | Submit and verify the challenge code |
POST | /two-factor/send-code | /two-factor/send-code | sendCode | Send (or resend) an email OTP on demand |
GET | /two-factor/setup | /panel-global/two-factor/setup | showSetup | Render the 2FA management/setup page |
POST | /two-factor/enable | /panel-global/two-factor/enable | enable | Generate TOTP secret and begin app setup |
POST | /two-factor/enable-email | /panel-global/two-factor/enable-email | enableEmail | Switch to email method and send confirmation code |
POST | /two-factor/confirm | /panel-global/two-factor/confirm | confirm | Confirm the first code to activate 2FA |
POST | /two-factor/method | /panel-global/two-factor/method | setMethod | Change the active method (requires re-confirmation) |
DELETE | /two-factor/disable | /panel-global/two-factor/disable | disable | Disable 2FA (requires current password) |
POST | /two-factor/cancel-setup | /panel-global/two-factor/cancel-setup | cancelSetup | Abort a pending (unconfirmed) setup |
The challenge routes (
GET/POST /two-factor/challenge and POST /two-factor/send-code) are intentionally registered outside the two_factor middleware group. They live only inside the auth middleware group. If they were placed inside the two_factor group, the middleware would intercept them — detecting an unverified session — and redirect back to the challenge, creating an infinite redirect loop. All other 2FA management routes (setup, enable, confirm, disable) are inside the two_factor group because the user must already have passed the challenge to reach their profile settings.How the Middleware Decides
TheEnsureTwoFactorAuthenticated middleware (two_factor) follows this decision tree on every request: