Skip to content

Multi-Factor Authentication

When require_mfa is true, every browser login applies the configured local MFA policy before establishing an SSO session or issuing an authorization code. This includes password login, signup, email verification, magic links, named and discoverable passkeys, and federation. A user who voluntarily enrolled in TOTP must also complete TOTP when global MFA is disabled. Client consent follows successful authentication.

Passkeys and federated logins count as primary authentication; they do not replace local TOTP or email OTP. This applies even when a passkey reports successful user verification, because the local MFA policy is independent of that authenticator. Magic links and email-verification links already prove email ownership, so they do not repeat an email OTP step. They still require TOTP when local policy selects it, including voluntary enrollment when global MFA is disabled. Trusted-device bypass follows the requesting client’s policy.

SSO and direct consent check the method recorded by the completed login against current MFA requirements. If MFA was enabled later, the session has no recorded proof, or the method no longer satisfies the policy, fresh authentication is required. With prompt=none, authorization returns login_required rather than bypassing MFA.

sequenceDiagram
    actor User
    participant Browser
    participant IdP as Autentico

    User->>Browser: Enter username + password
    Browser->>IdP: POST /oauth2/login
    IdP->>IdP: Credentials valid — create MFA challenge
    IdP->>Browser: 302 → /oauth2/mfa?challenge_id=X
    Browser->>IdP: GET /oauth2/mfa (render MFA page)
    IdP->>Browser: Show TOTP input (or send email OTP)
    User->>Browser: Enter OTP code
    Browser->>IdP: POST /oauth2/mfa (code + challenge_id)
    IdP->>IdP: Validate OTP — mark challenge used
    IdP->>Browser: 302 → redirect_uri?code=AUTH_CODE

The MFA challenge is a short-lived, single-use token (10 minutes). If it expires, the user is redirected back to the login page.

A verified challenge is consumed before issuing a session, authorization code or trusted-device cookie. Concurrent submissions can succeed only once. Used, expired, exhausted or changed challenges are rejected; a database failure while consuming the challenge cannot complete login.

TOTP uses time-based one-time passwords compatible with any RFC 6238 authenticator app — Google Authenticator, Authy, 1Password, Bitwarden, and others.

Enrollment happens automatically on first login after MFA is enabled. Users who have not yet set up TOTP are shown a QR code on the MFA page:

  1. Scan the QR code with an authenticator app
  2. Enter the 6-digit code from the app to confirm enrollment
  3. On all future logins, enter the code from the app

Each TOTP code can be accepted only once across browser login, password grants, and account actions. Enrollment confirmation also uses up its code, so wait for the authenticator to show the next code before signing in or disabling MFA. Codes use 30-second timesteps with one timestep of clock tolerance in either direction; a previously accepted or older timestep cannot be reused.

Enrolled TOTP has a shared account budget of five failed verifications across browser login, the password grant, and TOTP removal. Starting a new login challenge, changing IPs, or entering the correct password does not reset it. After five failures, even valid TOTP codes are rejected until account_lockout_duration expires (15 minutes when configured as zero or a negative duration). Successful TOTP verification resets the failure count. The account API and password grant return 403 mfa_locked during lockout; the browser redirects to login with a cooldown message. Password failures on removal do not spend TOTP attempts.

The TOTP secret is encrypted per-user in the database with AES-256-GCM and the persistent AUTENTICO_TOTP_ENCRYPTION_KEY from .env or the environment. Schema 17 encrypts existing enrollments in place, so existing authenticator apps keep working. Keep the original encryption key when restarting or restoring the database. The QR code is generated server-side — no third-party service is involved.

Issuer name displayed in the authenticator app comes from the theme_title setting.

Email OTP sends a one-time code to the user’s registered email address on each login. No enrollment step is required.

To use email OTP, set:

mfa_method = email

And configure SMTP:

smtp_host = smtp.example.com
smtp_port = 587
smtp_username = [email protected]
smtp_password = your-smtp-password
smtp_from = [email protected]

In the Admin UI: Settings → set require_mfa to true and choose mfa_method.

Via API:

Terminal window
curl -X PUT https://auth.example.com/admin/api/settings \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"require_mfa": "true", "mfa_method": "totp"}'

After a successful MFA verification, users can check “Trust this device” to skip MFA on future logins from the same browser. See Trusted Devices for MFA bypass on recognized devices.