Skip to main content

Multi-Factor Authentication (MFA)

MFA (multi-factor authentication) asks users for a second proof of identity after their first sign-in step. LumoAuth supports passkeys (sign-in with a device biometric or security key), push approval in the LumoAuth app, authenticator apps (TOTP, time-based one-time codes), text-message codes and email codes, plus single-use recovery codes for when a user loses their factors. Each factor has a strength tier; your organization's MFA policy decides who must use MFA, which factors are allowed and how strong they must be.

The model is "recommend, don't enumerate": LumoAuth proposes one best factor for the user's device, starts the user's default factor automatically at sign-in, and keeps the alternatives one click away. For risk scoring of individual sign-ins, see Adaptive MFA.


Factors and strength tiers​

Every enrolled factor is an authenticator with a type, a display name, a state and a tier. Tier 1 is strongest.

TierLabelFactor typeWhat the user doesamr values
T1StrongestpasskeyConfirms with fingerprint, face, screen lock or security key. Phishing-resistant.hwk, user
T2StrongpushTaps the matching number in the LumoAuth app, unlocked with biometrics or passcode.swk, user
T3StandardtotpTypes the 6-digit code from an authenticator app (Google Authenticator, 1Password, …).otp
T4Basicsms_otpTypes a 6-digit code received by text message.sms
T4Basicemail_otpTypes a 6-digit code received by email.otp
RecoveryRecoveryrecovery_codeTypes one of 10 single-use codes saved at enrollment. Recovery only.otp

Rules that follow from the tiers:

  • Email codes do not count as a second factor unless the organization turns on email_otp_counts_as_mfa. An inbox is often protected by the same password.
  • Recovery codes never satisfy a sign-in policy on their own. Using one starts account recovery.
  • A passkey counts as both factors (passwordless sign-in) while passwordless_passkey is on, which is the default.
  • When several factors qualify, LumoAuth challenges the user's default factor. If the default is weaker than the required tier, it picks the strongest factor the user holds that meets it.
  • A new factor becomes the default automatically when it is stronger than the current default (for example, adding a passkey to an account that used TOTP).

Authenticators are addressed by a reference of the form {type}:{id}, for example totp:12, passkey:7 or push:5. The MFA API uses these references as ids.


Requirement modes​

The organization's MFA requirement (policy key requirement) decides who is asked to enroll and whether sign-in can finish without a factor.

ModeUsers without a factorUsers with a factor
Off (off)Never prompted.Not challenged at sign-in, even if they set one up.
Optional (optional)Shown Secure your account after sign-in, with Remind me later (re-prompted after remind_interval_days).Challenged at sign-in unless the device is trusted or adaptive MFA grants a low-risk skip. Medium and high risk always challenge.
Risk-based (recommended) (risk_based, default)Not prompted at sign-in. They can add a factor any time from Sign-in & security. A risky sign-in follows the adaptive-auth setting Risky sign-ins by users without MFA (no_mfa_action: Allow, Prompt for MFA setup, Email verification or Block access).Same as Optional.
Required (required)Must enroll. During the grace period they can choose Remind me later; after it, sign-in does not complete until a factor is enrolled.Same as Optional. Cannot remove their last factor.
Optional vs. risk-based

The two modes differ only for users without a factor: Optional invites them to enroll after every sign-in (with a weekly Remind me later), Risk-based leaves them alone unless a sign-in looks risky, in which case no_mfa_action applies. Users with a factor are challenged the same way in both modes. Organizations that predate the MFA policy and did not require MFA are mapped to Risk-based.

Grace period and "Remind me later"​

  • With Optional or Required, Remind me later hides the prompt for remind_interval_days (default 7, range 1–90). Each deferral is recorded on the user and audited as mfa.enroll.deferred.
  • With Required, each user gets grace_period_days (default 14, range 0–90) counted from the later of their account creation and the moment the organization switched to Required. The screen shows the days left ("You have 5 days left to set it up"). With a grace period of 0, sign-in is blocked immediately for users without a factor.
  • required_factor_count = 2 makes the backup step mandatory: users with one factor are sent back to Secure your account after sign-in until they add a second one.

How LumoAuth decides at sign-in​

Password, magic-link, social, passkey and push sign-ins run the same decision after the first step succeeds:

  1. Recovery in progress. A user who recovered their account must enroll a new factor before anything else.
  2. Passkey sign-in. A passkey with user verification counts as both factors when passwordless_passkey is on and passkeys are allowed.
  3. Push sign-in. A number-matched push approval from the LumoAuth app counts as MFA.
  4. High risk. A high or critical adaptive risk level revokes all of the user's trusted devices and, if the user holds a passkey or push device, requires one of those (tier 2 or stronger).
  5. Policy off, or no factor yet. No challenge; the enrollment rules above apply (and, for a risky sign-in, no_mfa_action).
  6. Trusted device. A valid trusted-device cookie skips the challenge, unless risk is medium or high.
  7. Adaptive low-risk skip. Adaptive MFA may skip the challenge for a low-risk sign-in, unless risk is medium or high.
  8. Otherwise the user is sent to the MFA challenge.

Secure your account (enrollment)​

After sign-in, users who must or are invited to enroll (see Requirement modes) land on the Secure your account screen. The same screens open from Sign-in & security → Add a sign-in method:

/auth/secure-account

The enrollment screen with the recommended factor and the Other ways list expanded

The screen shows one recommended factor as the primary action with a one-line benefit, and every other allowed factor under a collapsed Other ways list. Factors the organization does not allow, or cannot deliver (no SMS provider configured, push not in the plan), are never shown.

  1. Passkey, when the browser reports a platform authenticator (Touch ID, Face ID, Windows Hello, Android screen lock). If it does not, the page promotes the next option as Recommended for this device.
  2. LumoAuth app (push), when the user is on a phone or already has the LumoAuth app enrolled on another device.
  3. Authenticator app (TOTP).
  4. LumoAuth app (push) on desktop, when not already recommended.
  5. Text message, only after the options above.
  6. Email code, last, and only when the organization allows it.

The screens​

StepScreenNotes
1Secure your accountRecommended factor + Other ways. A passkey is created right here through the OS prompt. Remind me later when the policy allows deferral.
2Factor setupOne screen per factor, with verification on the same screen (not needed for passkeys).
3Add a backup methodOffered once, recommending the next-best factor of a different type. Skip for now unless the organization requires two factors.
4Save your recovery codesTen codes with Copy, Download (.txt) and Print. The user ticks I've saved these codes to continue; they never retype a code. Skipped if the user already holds a set.
5Your account is protectedSummary, then the user continues to where they were going.

A passkey user therefore sees: Secure your account → Add a backup method → recovery codes → done. An authenticator-app user sees one extra screen (the TOTP setup).

Setting up each factor​

FactorRouteWhat happens
Passkey(on /auth/secure-account)Create a passkey opens the OS prompt. Cancelling returns to the same screen without an error, with Try again and the other ways still available. The passkey is named after the device and browser, for example "Mac – Chrome"; users can rename it later. See Passkeys.
LumoAuth app/auth/secure-account/pushDesktop shows a QR code; phones get an Open the LumoAuth app button. The app then receives a test notification with a number the user must tap; the page continues on its own once it is approved. See Push Authentication.
Authenticator app/auth/secure-account/totpQR code, Can't scan? (the key in groups of four) and, on phones, Open in authenticator app as the primary button. The user types the current code on the same screen. After three wrong codes the page suggests checking the phone's automatic date and time.
Text message/auth/secure-account/smsCountry preset from the browser language; the number is validated before anything is sent. A 6-digit code arrives by text and is entered on the same screen. See SMS & email.
Email code/auth/secure-account/emailOnly when email codes are allowed. A 6-digit code is sent to the account's email address.

A setup that is started but never verified expires after 15 minutes and is discarded. Adding a factor needs a recent sign-in (within 15 minutes) for a user's first factor, and a fresh verification with an existing factor for every later one (see Step-up).

The older enrollment URLs (/auth/mfa-enrollment, /auth/mfa-setup, /auth/mfa-enrollment/sms) redirect into this flow.


Signing in with a second factor​

The challenge page is at:

/login/mfa-challenge
  • Auto-start. The page starts the user's factor immediately: sends the push, sends the text or email, prepares the passkey prompt, or focuses the code field. The factor last used successfully on this browser is preferred over the default (remembered in the lumo_mfa_last cookie).
  • Try another way lists the user's other factors, strongest first, by display name (for example "Text •••• 4821"), plus Use a recovery code when the user has codes left.
  • Push. The page shows a two-digit number; the user taps the same number in the LumoAuth app. The page updates on its own when the request is approved. Resend sends a new request with a new number, and Use a code from the app instead accepts the app's offline 6-digit code. Push requests for a second factor expire after 60 seconds.
  • Trust this device for N days appears when the organization allows trusted devices (see Trusted devices).
  • I have a temporary access code from my admin accepts a code issued by an administrator (see Admin-assisted recovery).

Limits​

LimitDefault
Wrong answers per challenge5, then the user must request a new code or try another way
Failed MFA attempts per account10 in 15 minutes locks MFA for 15 minutes ("Verification paused") and emails the user
Push requests per user3 in 5 minutes; after that another factor is required
Denied push requests2 denials in 10 minutes suspend push for the user for 30 minutes
Code submissions on the hosted challenge page and the challenge API10 per 15 minutes per user, one shared budget; Resend does not count. The API answers 429 rate_limited with Retry-After
Text-message code6 digits, valid 5 minutes, 5 attempts; a new code replaces the old one
Email code6 digits, valid 10 minutes, 5 attempts

Error messages do not say whether a code was wrong or expired; they tell the user what to do next.

Upgrade nudge and security check-up​

After a sign-in with an authenticator app, text or email code, users who hold no passkey or push device see a one-click offer to add a passkey ("so you don't need codes next time") at /auth/mfa-upgrade, if the organization allows passkeys. Dismissing it hides it for nudge_interval_days (default 30). Thirty days after a user's first enrollment, the same page asks them once to confirm their sign-in methods and recovery codes are still available.

Tokens​

Tokens issued after the challenge carry amr (the first-factor method plus the factor used plus mfa) and an acr value for the strongest tier used. Applications can require a tier with acr_values. See ACR and AMR.


Step-up for sensitive actions​

Some actions need a fresh verification, within step_up_max_age_seconds (default 600, 10 minutes), even when the user is already signed in:

  • adding a sign-in method when the user already has one,
  • removing a sign-in method (with a different method where the user has one),
  • generating new recovery codes,
  • changing the password on Sign-in & security or the email address on Personal info (when the user has a factor; step-up comes before the current password is checked),
  • deleting the account (users without a password, such as social, SAML or passkey-only accounts, confirm with step-up alone),
  • issuing a temporary access code (the administrator's own verification),
  • downloading a personal data export or data-portability file.

In the browser the user is sent to:

/auth/step-up?target=/account/security

The step-up page starts the user's default factor, or their strongest one if the default is weaker than the organization's minimum strength for sensitive actions (min_tier_step_up, default T3 — authenticator app or stronger); Try another way only lists factors that are strong enough. It returns to target once the verification succeeds. A session that already passed MFA within the window is not asked again only when the factor used at that moment was at least that strong (a stronger factor used earlier in the session does not count), and the verification belongs to the signed-in user. A trusted device, an adaptive low-risk skip and a temporary access code satisfy the sign-in, never step-up.

Users without a second factor cannot step up. For these actions their session must have signed in within the last 10 minutes; otherwise they are asked to sign in again. API callers send the id of an approved step-up challenge (not a sign-in challenge) in the X-MFA-Challenge header instead; an application can ask for a stronger factor with min_tier but never a weaker one than the policy; see MFA API → Step-up.

For a push step-up, an application can pass a description of the action (for example "Approve transfer of $500 to •••• 1122"); the LumoAuth app shows it on the approval screen.


Trusted devices​

After passing the challenge, users can tick Trust this device for N days. The next sign-ins from that browser skip the challenge until the trust expires.

  • N is the organization's trusted_device_days (default 30, range 0–180). 0 turns the option off.
  • Trust is a signed, HttpOnly, Secure, SameSite=Lax cookie named lumo_td, bound to the user and backed by a server-side record the user can revoke.
  • Trust is revoked automatically when the user changes their password, removes a sign-in method, recovers their account, or signs in at high risk.
  • Medium or high risk always challenges, even on a trusted device, and sensitive actions always ask again.

Users see and revoke trusted devices under Sign-in & security (Forget, Forget all) or through the API.


Sign-in & security​

Users manage their factors themselves at:

/account/security

The Sign-in & security page with the Add a sign-in method menu open

The page shows a status (Your account is protected, Add a backup method or Your account is at risk), every sign-in method with its tier, when it was added and last used, and lets users add methods, rename them, choose the default and remove them. It also covers recovery codes, Trusted browsers and Sign out of all other devices. See Account self-service.


Recovery​

When a user cannot use their usual factor, the options are tried in this order:

  1. Another enrolled factor — Try another way on the challenge page.
  2. A recovery code — Use a recovery code.
  3. Admin-assisted recovery — a temporary access code from an administrator.

Recovery codes​

  • 10 codes, each 10 characters from an alphabet without look-alikes (no 0, O, 1, I or L), shown as XXXXX-XXXXX.
  • Each works once. Codes are compared case-insensitively and with or without the hyphen, and stored only as bcrypt hashes.
  • Generating a new set invalidates the old one. When 2 or fewer remain, the account page asks the user to generate a new set.
  • Codes issued before September 2026 (XXXX-XXXX-XXXX-XXXX) keep working until the user generates a new set.

What happens after a recovery​

Signing in with a recovery code or a temporary access code is treated as an account recovery. LumoAuth then:

  • signs out every other session and revokes all trusted devices,
  • emails the user that the account was recovered,
  • restricts the session to setting up a new sign-in method and saving new recovery codes; normal access resumes after that,
  • audits mfa.recovery.started, and mfa.recovery.completed once the new factor and recovery codes are in place.

Admin-assisted recovery​

Administrators cannot switch MFA off for a user. Instead they issue a temporary access code:

  1. Open the user in the organization portal (/orgs/{orgId}/portal/users), section Two-step verification.
  2. In the temporary access code form, enter a Reason (required, audited), choose Valid for (1 hour, 4 hours or 24 hours) and whether it is Single use, then Issue code.
  3. Confirm your own identity with MFA if asked.
  4. Give the code (12 characters) to the user over a channel you trust. It is shown once.

The Two-step verification section of a user's page with the temporary access code form

The user signs in with their password, picks I have a temporary access code from my admin on the challenge page and enters the code. The session can only enroll new factors. Only one live code exists per user; issuing another revokes the previous one. Issuing a code is audited as mfa.tap.issued and the user is emailed.

From the same section, an administrator can Remove a single lost authenticator (for example a stolen phone). The user is notified, their trusted devices are revoked, and the organization's policy still applies at their next sign-in.

The same actions are available in the Admin MFA API. The old POST /admin/users/{userId}/mfa/reset endpoint returns 410 Gone.


Configuring the MFA policy​

Navigate to Security → Sign-in settings at:

/orgs/{orgId}/portal/settings/authentication

The MFA policy section with enrollment coverage and the Requirement options

The MFA policy section shows enrollment coverage (enrolled users, users at T1–T2, users in and past the grace period, and how many chose "Remind me later") and the policy settings. Changes take effect at each user's next sign-in; existing sessions are not ended. Every change is audited as mfa.policy.changed.

The same policy is available as JSON through GET/PUT /orgs/{orgId}/api/v1/admin/policies/mfa (reference).

Requiring MFA for one application​

Applications → your app → Security → Authentication → Require multi-factor authentication (saved with Save auth settings) makes /authorize issue a code for that application only when the session passed a second factor for this user (a verified factor, a trusted device or an adaptive low-risk skip). Otherwise the user is sent to step-up, which offers enrollment when they have no factor yet, and then returns to /authorize. With prompt=none the application gets interaction_required instead. When the organization policy is Required the switch is on and locked. Which factors count is set by the organization's MFA policy.

The Authentication section on an application's Security tab with Require multi-factor authentication

The older per-application Enable MFA and Enterprise SSO switches were removed: they were never enforced. Factors and identity providers are organization settings.

Policy reference​

KeyPortal labelValuesDefault
requirementRequirementoff, optional, risk_based, requiredrisk_based
allowed_factorsAllowed factorsAny of passkey, push, totp, sms_otp, email_otppasskey, push, totp, sms_otp
min_tier_loginMinimum strength at sign-in1–4 (factors weaker than this do not count at sign-in)4
min_tier_step_upMinimum strength for sensitive actions1–43
passwordless_passkeyA passkey counts as both factorstrue / falsetrue
required_factor_countFactors each user must have1 or 21
grace_period_daysEnrollment grace period (days)0–9014
remind_interval_daysRe-prompt after "Remind me later" (days)1–907
trusted_device_days"Trust this device" period (days)0–180 (0 disables)30
email_otp_counts_as_mfaEmail codes count as a second factortrue / falsefalse
sms_block_voipBlock internet (VoIP) phone numberstrue / falsefalse
sms_country_allowlistSMS: only these countriesISO 3166 alpha-2 codes; empty = all[]
sms_country_denylistSMS: never these countriesISO 3166 alpha-2 codes[]
nudge_interval_days(API only)1–36530
step_up_max_age_seconds(API only)60–3600600

Values outside a range are clamped to it; unknown factor types are dropped, and an empty allowed_factors falls back to the default. Text messages can only be allowed once an SMS provider is configured, and push only on plans that include it; factors the organization cannot deliver are removed from the effective list. The read-only required_since key records when the requirement last switched to Required.

Legacy keys

Organizations created before this policy existed used mfa_required and mfa_sms_enabled in their authentication settings. Until an MFA policy is saved, LumoAuth derives one from them (mfa_required: true → Required; mfa_sms_enabled: true → text messages allowed). Saving the policy writes mfa_policy and keeps mfa_required and mfa_sms_enabled in sync for older integrations. Writing mfa_required or mfa_sms_enabled through the authentication settings API is translated into a policy change (mfa_required: true → Required, false → Risk-based; mfa_sms_enabled adds or removes sms_otp).

The same page keeps the adaptive-auth setting Risky sign-ins by users without MFA (no_mfa_action: Allow, Prompt for MFA setup, Email verification or Block access). It applies when adaptive MFA wants a challenge for a user who has no factor, and is how Risk-based handles unenrolled users.


Notifications​

Users are emailed when:

  • a sign-in method is added or removed (other enrolled phones also get a push),
  • recovery codes are generated or used,
  • MFA is locked after repeated failures,
  • they report a push request with Deny – this wasn't me,
  • an administrator issues them a temporary access code,
  • their account is recovered.

Most of these emails include a This wasn't me link (valid 7 days) to /auth/not-me. Confirming it signs out every session and revokes all trusted devices; a user who has no sign-in method left must set one up at the next sign-in.

All MFA activity is written to the audit log and delivered to webhooks as mfa.* events. See Audit logs.


What MFA does not include​

  • Codes are delivered by text message or email only; there is no voice-call delivery, and phone line type comes from numbering-plan data rather than a live carrier lookup.
  • There is no SMS-pumping detection beyond the rate limits, and one SMS provider per organization, so no failover between providers.
  • Account recovery is through recovery codes or an administrator-issued temporary access code; there is no self-service recovery by email plus phone.
  • A temporary access code is issued by one administrator; there is no two-administrator approval.
  • Changing a phone number and viewing API credentials do not trigger step-up.
  • There are no "new device sign-in" notification emails.
  • The MFA policy is organization-wide: there are no per-group or per-application overrides (an application can only require MFA).
  • Push approvals are handled by the LumoAuth app only; there is no way to approve pushes from your own app, and no account transfer between phones.
  • MFA events reach a SIEM (security information and event management system) through the log sinks like every other audit event; there is no separate MFA event stream.

Best practices​

  1. Keep the default Risk-based requirement and turn on Adaptive MFA, or use Required with a grace period for workforce organizations.
  2. Leave passkeys and push allowed: they are the strongest factors and the fastest to use.
  3. Raise Minimum strength for sensitive actions to T2 or T1 if your users hold passkeys or the LumoAuth app.
  4. Keep email codes as a recovery aid, not a second factor.
  5. Point helpdesk staff at temporary access codes; there is no way to switch MFA off for a user.
  6. Watch coverage in the portal and the mfa.push.reported_fraud and mfa.locked events.