Multi-Factor Authentication
AuthGate supports multi-factor authentication (MFA) with TOTP authenticator apps, SMS codes, and backup codes. MFA adds a second verification step after primary authentication to protect user accounts.
Overview
MFA in AuthGate works as a second step after primary authentication (OAuth, email, SMS, or magic link). When MFA is enabled for a user, the primary auth step returns a challenge token instead of a JWT. The user must then complete the MFA challenge to receive their JWT and refresh token.
Key characteristics:
- Three MFA methods — TOTP (authenticator apps), SMS codes, and backup codes
- Per-project policies — off, optional, or required
- Challenge-based flow — primary auth returns a challenge, MFA verification returns the JWT
- Backward-compatible — when MFA is "off", existing JWT-only clients continue working unchanged
- Rate-limited — MFA verification is limited to 5 attempts per 15 minutes per IP
MFA is separate from SMS-based primary authentication. SMS as an MFA method sends a code to the user's registered phone number as a second factor, while SMS primary auth uses the phone number as the sole authentication method.
MFA policies
MFA policy is configured per project and determines whether users are required to set up MFA.
- Name
off- Type
- string
- Description
MFA is disabled for the project. Primary authentication returns a JWT directly. This is the default policy and is fully backward-compatible with existing clients.
- Name
optional- Type
- string
- Description
Users may choose to enable MFA for their account. If enabled, they are challenged on sign-in. If not enabled, primary auth returns a JWT directly.
- Name
required- Type
- string
- Description
All users must set up MFA before they can complete authentication. After first sign-in, users are prompted to enroll in at least one MFA method.
When the policy is optional or required, the authentication response changes depending on whether the user has MFA enrolled:
- No MFA enrolled (optional policy): returns
{ token, refresh_token, user }as normal - MFA enrolled: returns
{ challenge, methods }instead of a JWT - No MFA enrolled (required policy): returns
{ challenge, enrollment_required: true }to prompt setup
TOTP setup flow
TOTP (Time-based One-Time Password) works with authenticator apps like Google Authenticator, Authy, or 1Password. The setup flow has three steps:
1. Generate TOTP secret
Call the TOTP setup endpoint with a valid JWT to generate a secret and QR code.
curl -X POST https://auth.example.com/api/proxy/mfa/totp/setup \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
The response includes everything needed to configure the authenticator app:
{
"secret": "JBSWY3DPEHPK3PXP",
"qr_code": "data:image/png;base64,iVBORw0KGgo...",
"uri": "otpauth://totp/AuthGate:jane@example.com?secret=JBSWY3DPEHPK3PXP&issuer=AuthGate"
}
Display the qr_code as an image for the user to scan, or show the secret for manual entry.
2. Verify setup
After the user scans the QR code and enters the code from their authenticator app, verify it:
curl -X POST https://auth.example.com/api/proxy/mfa/totp/verify-setup \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
{
"success": true
}
This confirms the TOTP secret is correctly configured and enrolls the user in TOTP MFA.
3. Generate backup codes
After TOTP is verified, generate backup codes for the user:
curl -X POST https://auth.example.com/api/proxy/mfa/backup-codes/generate \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
{
"codes": [
"a1b2c3d4",
"e5f6g7h8",
"i9j0k1l2",
"m3n4o5p6",
"q7r8s9t0",
"u1v2w3x4",
"y5z6a7b8",
"c9d0e1f2",
"g3h4i5j6",
"k7l8m9n0"
]
}
Display all 10 backup codes to the user and instruct them to store them securely. Each backup code can only be used once. Generating new backup codes invalidates all previous codes.
Challenge flow
When a user with MFA enrolled signs in, the primary authentication step returns a challenge instead of a JWT. The challenge must be verified with an MFA code to complete authentication.
Step 1: Primary authentication
The user signs in with their primary method (e.g., email + password):
curl -X POST https://auth.example.com/api/proxy/email/signin \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"password": "securepassword",
"callback_url": "https://myapp.com/auth/callback"
}'
Instead of a JWT, the response indicates that MFA is required:
{
"mfa_required": true,
"challenge": "ch_a1b2c3d4e5f6...",
"methods": ["totp", "sms", "backup_codes"]
}
- Name
mfa_required- Type
- boolean
- Description
Always
truewhen MFA verification is needed.
- Name
challenge- Type
- string
- Description
An opaque challenge token. Pass this to the MFA verify endpoint. Expires in 5 minutes.
- Name
methods- Type
- string[]
- Description
The MFA methods the user has enrolled. Use this to display the appropriate UI.
Step 2: MFA verification
Submit the challenge token and the user's MFA code:
curl -X POST https://auth.example.com/api/proxy/mfa/verify \
-H "Content-Type: application/json" \
-d '{
"challenge": "ch_a1b2c3d4e5f6...",
"code": "123456",
"method": "totp"
}'
On success, the response includes the JWT, refresh token, and user info:
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "rt_a1b2c3d4e5f6...",
"user": {
"id": "user_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"email_verified": true
}
}
The complete flow is: primary auth -> MFA challenge -> MFA verify -> JWT + refresh token.
SMS fallback
If a user has SMS as an MFA method, they can receive a verification code via SMS during the challenge flow.
Send SMS code
After receiving the challenge, request an SMS code:
curl -X POST https://auth.example.com/api/proxy/mfa/sms/send-code \
-H "Content-Type: application/json" \
-d '{"challenge": "ch_a1b2c3d4e5f6..."}'
{
"success": true
}
The user receives a 6-digit code via SMS. They then verify it using the standard MFA verify endpoint with "method": "sms".
Enable SMS MFA
Users can enable SMS as an MFA method if they have a verified phone number on their account:
curl -X POST https://auth.example.com/api/proxy/mfa/sms/enable \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
{
"success": true
}
Backup codes
Backup codes are 10 single-use codes that allow authentication when other MFA methods are unavailable (e.g., lost phone).
- 10 codes generated at a time
- Single-use — each code is consumed on use
- Regeneration invalidates all existing codes
- Use method
"backup_codes"with the MFA verify endpoint
curl -X POST https://auth.example.com/api/proxy/mfa/verify \
-H "Content-Type: application/json" \
-d '{
"challenge": "ch_a1b2c3d4e5f6...",
"code": "a1b2c3d4",
"method": "backup_codes"
}'
When a user's backup code count is low, prompt them to generate new codes. Generating new codes replaces all existing codes, so the user must save the entire new set.
Dashboard configuration
MFA is configured per project in the AuthGate dashboard under Settings > Security > Multi-Factor Authentication.
Available settings
- Name
MFA policy- Type
- off | optional | required
- Description
Controls whether MFA is disabled, opt-in, or mandatory for all users. Defaults to "off".
- Name
Allowed methods- Type
- string[]
- Description
Which MFA methods are available to users. Choose from TOTP, SMS, and backup codes. Backup codes are always enabled when any other method is active.
Changing policies
- off -> optional: No impact on existing users. Users can opt in to MFA at their own pace.
- off -> required: Existing users will be prompted to enroll in MFA on their next sign-in.
- required -> optional: Users who already enrolled keep their MFA. Users who haven't enrolled are no longer forced to.
- required -> off: MFA is disabled for all users. Existing enrollments are preserved but not enforced.
Changing the MFA policy does not delete existing MFA enrollments. If you switch from "required" to "off" and back to "required", users who previously enrolled will not need to set up MFA again.