Docs / Login security

Two-factor authentication

Configure passkeys, authenticator apps and email verification by role, with recovery codes and enforcement.

All documentation

A second factor means a stolen or guessed password is not enough to get in. FluentAuth supports three methods and applies them per role, so administrators can be required to use one while subscribers are never asked.

Methods

MethodHow the code arrivesBest for
PasskeyA browser prompt for your device, security key or password managerDomain-bound device verification; see Passkeys
Email codeA one-time code emailed to the user’s addressSites where everyone can read their email at login time; no app to install
Authenticator app (TOTP)A six-digit code from Google Authenticator, 1Password, Authy, Microsoft Authenticator or any TOTP appAnyone who can carry a phone; works offline and does not depend on email delivery

A usable passkey is offered ahead of an authenticator app. Enrolled device methods take priority over email. A magic link already proves access to the mailbox, so it skips an email code but does not satisfy a required device factor.

SINCE 3.0

Passkeys, authenticator-app codes, recovery codes, the enrolment list and required-role enforcement were added in FluentAuth 3.0.0. Email codes have been available since 1.0.

Two-Factor Authentication settings showing authenticator apps, email codes and required-role policy.

Settings

Under Settings → General Settings → Two-Factor Authentication:

SettingMeaning
Email CodeOn or off
Roles that get the codeRoles that must enter an emailed code (default: Administrator, Editor, Author)
Authenticator AppOn or off
Roles allowed to set one upRoles invited to enrol an authenticator voluntarily
Make it required → Roles it is required forRoles that must meet the policy; these users may enrol an enabled device method even if absent from its optional role list
What they can useA passkey or the authenticator app only, or Any method, including an emailed code

Passkeys have their own Passkeys section immediately above this one; see Passkeys. Enable the methods first, then choose required roles under Make it required, and select Save. A requirement only applies when an enabled method can satisfy the selected policy. Email codes count for the second option only when enabled for the user’s role. With every method off, there is no enforceable requirement.

A role can be in both email and authenticator lists. Users in that role get email codes until they enrol an app, then app codes.

Enrolling an authenticator

Users enrol from their own profile (Users → Profile → Two-Factor Authentication), or from the standalone setup page at wp-login.php?action=fls_2fa_setup, which works without wp-admin access. Enrolment is:

  1. Scan the QR code, or type the secret into the app by hand.
  2. Enter the current six-digit code to prove the app is set up.
  3. Save the ten recovery codes shown once. Each works one time in place of an app code.

The QR code is drawn by the plugin itself. Nothing about the secret leaves your server.

Encrypting stored secrets

An authenticator secret is the one credential FluentAuth has to be able to read back, because a code is checked by generating it. It cannot be hashed the way a password or a recovery code is. So it can be encrypted instead, with a key you keep out of the database:

  1. Add FLUENT_AUTH_SECURITY_KEY to wp-config.php. Any long random string will do.
  2. Turn Encrypt authenticator app data on under the authenticator app settings.

Existing enrolments are encrypted in place, and a copy of your database stops being enough to generate anybody’s codes. If the key later goes missing or changes, FluentAuth says which of the two happened and both are recoverable while you still have the old value. See Constants.

What the user sees

After a correct password, a second screen asks for the code. Five wrong codes end the attempt, and a pending challenge expires after ten minutes. Both outcomes are logged.

Users in Roles it is required for who lack a method satisfying the policy must complete setup before signing in. Existing sessions are gated from admin access until they do. A role already receiving email codes meets Any method, including an emailed code without device enrolment; choose the device-only option to require a passkey or authenticator app. Users whose role may enrol but is not required see a one-time nudge after login, dismissible; the fluent_auth/ask_to_set_up_totp filter turns that off.

Managing enrolments

Settings → 2FA Enrollment lists eligible users and their enrolled device methods, searchable and filterable. Review the enrolled methods and reset the affected method for someone who has lost access. Passkeys can also be managed from the user profile.

Logins that skip the form

FluentAuth also checks application-password authentication, social logins and code that calls wp_set_auth_cookie() directly. A headless request that needs a challenge cannot complete that interactive step and is refused. Low-risk headless logins that do not need a challenge remain available. A required device policy still applies to magic links and social login. To exempt a specific integration, use the fluent_auth/enforce_2fa_on_auth_cookie filter.

WP-CLI is exempt, so wp user session create and similar still work from the server.

Recovery

  • Lost the phone: use a recovery code, then reset and re-enrol from the profile.
  • Lost the phone and the codes: another administrator resets you from 2FA Enrollment.
  • Only administrator, lost everything: see Troubleshooting for the server-access recovery procedure.

Developer hooks

HookPurpose
fluent_auth/2fa_challenge_required (filter)Require a challenge for a user whose role does not. It cannot waive one
fluent_auth/2fa_methods (filter)Add or remove methods
fluent_auth/totp_enabled (filter)Offer the authenticator app to this user, or not
fluent_auth/totp_activated, totp_disabled (actions)React to enrolment events
fluent_auth/recovery_codes_generated, recovery_code_used (actions)A set was issued, or one was spent
fluent_auth/2fa_code_request_limit, 2fa_code_request_timing (filters)Rate limit for emailed codes