Multi-Factor Authentication (MFA)
TOTP-based two-factor authentication compatible with Google Authenticator, Authy, 1Password, and other authenticator apps. Built on the pragmarx/google2fa-laravel package.
How It Works
- User enables MFA from their profile settings
- A TOTP secret is generated and displayed as a QR code
- User scans the QR code with their authenticator app
- User confirms by entering a 6-digit code from the app
- 8 recovery codes are generated as a backup
- On subsequent logins, a 6-digit code is required after entering credentials
Enable MFA
Step 1: Generate Secret
POST /api/auth/mfa/enable
Generates a TOTP secret and returns it along with a QR code URL for the authenticator app.
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_code_url": "otpauth://totp/SaasKitFy:john@example.com?secret=JBSWY3DPEHPK3PXP&issuer=SaasKitFy",
"qr_code_svg": ""
}
At this stage, MFA is not yet active. The user must confirm with a valid code.
Step 2: Confirm Setup
POST /api/auth/mfa/confirm
{
"code": "123456"
}
Validates the 6-digit code against the pending secret. On success, MFA is activated and 8 single-use recovery codes are returned.
{
"message": "MFA has been enabled.",
"recovery_codes": [
"abcde-fghij",
"klmno-pqrst",
"uvwxy-zabcd",
"efghi-jklmn",
"opqrs-tuvwx",
"yzabc-defgh",
"ijklm-nopqr",
"stuvw-xyzab"
]
}
A MfaStatusNotification is sent to the user confirming MFA has been enabled.
Verify MFA on Login
POST /api/auth/mfa/verify
{
"mfa_token": "temp_token_from_login",
"code": "123456"
}
When a user with MFA enabled logs in, the login endpoint returns a short-lived mfa_token (valid for 10 minutes) instead of a full session token. The client must call this endpoint with the token and a valid 6-digit TOTP code.
Using a Recovery Code
If the user cannot access their authenticator app, they can use one of their 8 recovery codes instead of a TOTP code:
{
"mfa_token": "temp_token_from_login",
"code": "abcde-fghij"
}
Recovery codes are single-use — each code is consumed when used and cannot be reused. The system automatically detects whether the submitted code is a 6-digit TOTP code or a recovery code.
Disable MFA
POST /api/auth/mfa/disable
{
"code": "123456"
}
Requires a valid TOTP code to confirm the action. Clears the TOTP secret and all recovery codes. A MfaStatusNotification is sent confirming MFA has been disabled.
Regenerate Recovery Codes
POST /api/auth/mfa/recovery-codes
{
"code": "123456"
}
Requires a valid TOTP code. Invalidates all existing recovery codes and generates a fresh set of 8 codes. Use this if the user has lost their recovery codes or has used several of them.
{
"recovery_codes": [
"newco-de001",
"newco-de002",
"newco-de003",
"newco-de004",
"newco-de005",
"newco-de006",
"newco-de007",
"newco-de008"
]
}
Recovery Code Storage
Recovery codes are encrypted at rest using Laravel's encryption. They are stored in the mfa_recovery_codes column on the User model as an encrypted JSON array. When a code is consumed, it is removed from the array.
Organization-Wide Enforcement
Organization owners and admins can require MFA for all members of their organization:
- Set
Organization.mfa_requiredtotrue - The
mfa_enforced_attimestamp records when enforcement began - Members without MFA are prompted to set it up on their next login
- Members cannot access organization resources until MFA is configured
- The enforcement check runs via middleware on all org-scoped routes
Admin Toggle
MFA can be globally enabled or disabled from the admin panel:
- Setting:
auth.mfa→enabledordisabled - When disabled globally, MFA setup endpoints return
403 Forbidden - Users who already have MFA enabled are not affected — they can still verify and disable
- Organization-level enforcement is independent of the global toggle