Contextual Multi-Factor Authentication (MFA)

This page explains why and how FusionAuth asks for MFA during the login process.

For guidance on implementing MFA using FusionAuth, see the MFA documentation.

When is MFA Required?#

FusionAuth displays an MFA challenge to the user when a login attempt meets one of the following criteria:

  • Does the tenant or application enable or require MFA?
  • Has the user provided an MFA method?
  • Did the user log in using an Identity Provider such as Google or OIDC? In this case, MFA is never required.
  • Has the device passed appropriate contextual checks, such as using a known device?

The following diagram shows the FusionAuth MFA logic:

graph TD
    isPolicyEnabled[Is MFA Turned On For This Tenant?] --> |No| noChallengePolicy[Do Not Challenge User]
    isPolicyEnabled --> |Yes| identityProviderLogin[User Logged In Using an Identity Provider?]
    identityProviderLogin --> |Yes| noChallengeIdentityProvider[Do Not Challenge User]
    identityProviderLogin --> |No| isUserMFAEnabled[Has the User Set Up MFA?]
    isUserMFAEnabled --> |No| isUserMFARequired[Is MFA Required By Tenant Or Application?]
    isUserMFAEnabled --> |Yes| devicePassesContextualCheck[Does the Device Pass Contextual Check?]
    devicePassesContextualCheck--> |Yes| noChallengeContextualCheck[Do Not Challenge User]
    devicePassesContextualCheck--> |No| challengeContextualCheck[Challenge User]
    isUserMFARequired --> |No| noChallengeMFARequired[Do Not Challenge User]
    isUserMFARequired --> |Yes| promptMFASetupRequired[Prompt MFA Setup]

    style noChallengePolicy stroke:#00FF00,stroke-width:4px
    style noChallengeIdentityProvider stroke:#00FF00,stroke-width:4px
    style noChallengeMFARequired stroke:#00FF00,stroke-width:4px
    style noChallengeContextualCheck stroke:#00FF00,stroke-width:4px

    style promptMFASetupRequired stroke:#FF0000,stroke-width:4px
    style challengeContextualCheck stroke:#FF0000,stroke-width:4px

High level diagram of MFA logic.

Contextual Checks#

Contextual checks are based on attributes of the request evaluated every time a user authenticates. These checks include:

  • Has this device been seen before?
  • Has this user been seen before on this device?
  • Is there a suspicious login detected for this authentication attempt?

The duration of trust of the device can be configured with the tenant.externalIdentifierConfiguration.twoFactorTrustIdTimeToLiveInSeconds configuration value.

The goal of this contextual check is to challenge the user for another factor of authentication whenever FusionAuth determines the risk of invalid access outweighs user friction.

Depending on your license, you can configure MFA policies at both the tenant and application level.

Tenant MFA Configuration#

Tenant configuration applies to all applications within a tenant. The MFA policy has three values:

  • Disabled: no MFA challenge occurs
  • Enabled: an MFA challenge occurs only if the user has a valid MFA method
  • Required: an MFA challenge always occurs, requires users without MFA to configure an MFA method

Here's a diagram of the MFA challenge logic when the tenant has a policy for MFA.

graph TD
    applicationMfaPolicy[MFA Policy At Application Level Configured?] --> |True| applicationMfaPolicyControls[Application MFA Policy Controls]
    applicationMfaPolicy --> |False| tenantMfaPolicy
    tenantMfaPolicy[What is MFA Policy At Tenant Level?] --> |Disabled| noChallenge[Do Not Challenge User]
    tenantMfaPolicy --> |Enabled| checkUserConfigEnabled[Check User Has MFA Method]
    tenantMfaPolicy --> |Required| checkUserConfigRequired[Check User Has MFA Method]
    checkUserConfigEnabled --> |MFA Configured| contextualMFACheck[Check Request Context]
    checkUserConfigEnabled --> |No MFA Configured| noChallengeUserConfig[Do Not Challenge User]
    checkUserConfigRequired --> |MFA Configured| contextualMFACheckRequired[Check Request Context]
    checkUserConfigRequired --> |No MFA Configured| forceUserToSetupMFA[Prompt User To Set Up MFA]
    contextualMFACheck --> |Context Check Fails| challenge[Challenge User]
    contextualMFACheck --> |Context Check Succeeds| noChallengeContextual[Do Not Challenge User]
    contextualMFACheckRequired --> |Context Check Fails| challengeRequired[Challenge User]
    contextualMFACheckRequired --> |Context Check Succeeds| noChallengeContextualRequired[Do Not Challenge User]

    style noChallengeContextualRequired stroke:#00FF00,stroke-width:4px
    style noChallenge stroke:#00FF00,stroke-width:4px
    style noChallengeContextual stroke:#00FF00,stroke-width:4px
    style noChallengeUserConfig stroke:#00FF00,stroke-width:4px
    style challengeRequired stroke:#FF0000,stroke-width:4px
    style challenge stroke:#FF0000,stroke-width:4px
    style forceUserToSetupMFA stroke:#FF0000,stroke-width:4px

Tenant MFA decision logic.

Application MFA Configuration#

Application configuration applies to a single application within a tenant. Application policies always supersede the tenant policy.

With an active application MFA configuration, there is an MFA policy with three possible values:

  • Disabled: no MFA challenge occurs
  • Enabled: an MFA challenge occurs only if the user has a valid MFA method
  • Required: an MFA challenge always occurs, requires users without MFA to configure an MFA method

An additional trust policy determines if an application accepts the results of other MFA challenges with the following options:

  • Any: any application's challenge results are acceptable
  • This: only this application's challenge results are acceptable
  • None: no application's challenge results are acceptable, always displays an MFA challenge

Here's a table outline possible scenarios for different trust policies.

Trust PolicyExample ApplicationNotes
AnyApps in a suite of applications such as Google Drive, Google Calendar and GmailAny MFA challenge is good enough since they all have roughly the same risk profile.
ThisA gambling application which uses real money in a suite which has other fantasy gaming apps.The other fantasy gaming apps might not require MFA at all, but if they do, it's not a high enough level of security for the "real money" gambling application.
NoneAn internal admin dashboardAccess should be strictly controlled and user friction is not an issue.

Here's a diagram of MFA challenge logic when the application has a policy for MFA.

graph TD
    applicationMfaPolicy[What is MFA Policy At Application Level?] --> |No Application Policy Present| deferToTenant[Defer To The Tenant Policy]
    applicationMfaPolicy --> |Disabled| noChallenge[Do Not Challenge User]
    applicationMfaPolicy --> |Enabled| checkUserConfigEnabled[Check User Has MFA Method]
    checkUserConfigEnabled --> |MFA Configured| contextualMFACheck[Check Request Context]
    checkUserConfigEnabled --> |No MFA Configured| noChallengeNoMFAConfigured[Do Not Challenge User]
    applicationMfaPolicy --> |Required| checkUserConfigRequired[Check User Has MFA Method]
    checkUserConfigRequired --> |MFA Configured| contextualMFACheckRequired[Check Request Context]
    checkUserConfigRequired --> |No MFA Configured| forceUserToSetupMFA[Prompt User To Set Up MFA]
    contextualMFACheck --> |Context Check Fails| challenge[Challenge User]
    contextualMFACheck --> |Context Check Succeeds| trustPolicyMFACheck[What is the Application MFA Trust Policy?]
    contextualMFACheckRequired --> |Context Check Fails| challengeRequired[Challenge User]
    contextualMFACheckRequired --> |Context Check Succeeds| trustPolicyMFACheck
    trustPolicyMFACheck --> |Any| noChallengeTrustPolicyAny[Do Not Challenge User]
    trustPolicyMFACheck --> |This Application| trustSource[Is MFA Trust From This Application?]
    trustSource --> |This Application| noChallengeAppMatch[Do Not Challenge User]
    trustSource --> |Any Other Application| challengeTrustSource[Challenge User]
    trustPolicyMFACheck --> |None| challengeTrustPolicyNone[Challenge User]

    style noChallenge stroke:#00FF00,stroke-width:4px
    style noChallengeNoMFAConfigured stroke:#00FF00,stroke-width:4px
    style noChallengeTrustPolicyAny stroke:#00FF00,stroke-width:4px
    style noChallengeAppMatch stroke:#00FF00,stroke-width:4px

    style challenge stroke:#FF0000,stroke-width:4px
    style challengeRequired stroke:#FF0000,stroke-width:4px
    style challengeTrustSource stroke:#FF0000,stroke-width:4px
    style challengeTrustPolicyNone stroke:#FF0000,stroke-width:4px
    style forceUserToSetupMFA stroke:#FF0000,stroke-width:4px

Diagram of application MFA decision logic.

License Limitations#

Not all plans support all MFA features. The plan you are on affects the MFA options available to you and your users. Learn more about plan features and pricing.

The following contextual MFA features are limited to the specified plan.

Enterprise Only#

  • Application MFA policies
  • Suspicious Login Contextual Check

Any Paid Plan#

  • Email MFA
  • SMS MFA
  • User MFA Enrollment Account Management Pages

Any Plan#

  • TOTP MFA
  • Tenant MFA policies
  • User MFA Enrollment APIs
  • Application MFA policies for the FusionAuth Admin UI only

MFA Challenges After Identity Provider Login#

If a user logs in with an Identity Provider such as Google or OIDC, FusionAuth does not challenge for MFA. FusionAuth trusts that the correct MFA challenge process happens at Identity Provider.

You can override this by using an MFA requirement lambda and examining the authenticationType parameter.

Intelligent MFA#

Available since 1.68.0

This feature is only available in paid plans. To learn more, see our pricing page.

Intelligent MFA uses risk signals integrated into FusionAuth to determine the risk level of a login attempt. Risk signals include (but are not limited to) the following:

  • BotDetected - The browser library signals that the user is a bot.
  • BlocklistedIp - The IP address is blocklisted.
  • DormantAccount - The user hasn't logged in for a long time.
  • DormantPassword - The password hasn't been changed for a long time.
  • ImpossibleTravel - The distance between recent logins exceeds the possible value a person can travel within the allotted time frame.
  • RecentIdentityChange - The user recently changed their identity.
  • RecentPasswordChange - The user recently changed their password.
  • SuspiciousUserAgent - The user agent is suspicious.
  • UnrecognizedDevice - The device has not been recognized.
  • UntrustedDevice - The device is not trusted.

To use these risk signals when determining whether to present an MFA challenge to a user, you can:

  • choose a tenant login policy of ChallengeOnHighRisk or ChallengeOnMediumRisk
  • manually use these signals or the aggregate risk level (LOW, MEDIUM, or HIGH) using the context of an MFA requirement lambda.

Roll Out Intelligent MFA#

Every user population is different. With Intelligent MFA, you want to reduce challenges for legitimate users, but still challenge when our proprietary algorithm indicates a possibility of an attacker or account takeover.

The rollout options differ by plan, because only the Enterprise plan can observe risk without acting on it.

With an Enterprise Plan#

On the Enterprise plan, any configured MFA requirement lambda logs risk and riskSignals to the event log when debug is enabled. You can use this behavior to measure first, then enforce.

  1. Collect a baseline.
    • Enable the user.two-factor.challenge, user.two-factor.success, and user.two-factor.failed_attempt webhooks and collect MFA metrics. You can also read the numbers on the Reactor tab.
    • Calculate your current challenge rate, which is the number of MFA challenges per DAU or logins. You can pull the denominator using the reporting API or the login API, respectively.
    • You can also collect the number of MFA-related customer support requests.
  2. Gather the risk scores but do not apply the policy.
    • Enable an MFA requirement lambda, but mimic your existing policy so that behavior does not change for your users; see below for sample lambdas.
    • Enable the debug toggle on the tenant Multi-Factor tab to log the user Id, composite risk, and riskSignals. If you do not already use an MFA requirement lambda, adding one means the MFA requirement decision to the lambda. You can use the sample MFA requirement lambdas below.
  3. Capture the logs.
    • Set up an event log create webhook to capture the logs. Discard any that are not MFA composite risk log events. Store the values, including the composite score and the user Id in a database to analyze.
  4. Analyze the logs.
    • From the log data, compare the count of HIGH and MEDIUM composite scores to the count of challenges in your baseline.
    • Review a sample of HIGH scores to see if they are legitimate users and LOW scores to see if they are not. The two risk policies ignore "trust this device", so users currently skipped by a trusted device are re-evaluated on risk and may be challenged.
  5. Pilot risk-based MFA.
    • Apply an application-level risk-based policy to one non-critical application.
    • Modify the lambda to challenge on HIGH risk for this application.
    • Keep reviewing the log data and verify challenge volume matches the shadow estimate.
    • Collect the number of MFA related customer support requests and compare it to the baseline, scaled based on the relative number of logins for this application.
  6. Widen risk based MFA rollout.
    • Expand the application based configuration to more applications. Choose whether to challenge on high or medium/high per application. Use the MFA requirement lambda for any per-application exceptions.
    • Turn off MFA debugging as you roll risk scoring MFA out further and have more confidence in how your users interact with the MFA challenge.
  7. Monitor the rollout.
    • Continue to calculate your challenge and challenge success rates.
    • You can also watch the suspicious login webhook (the threatsDetected value) for trends in which threats your users detect.

To roll back at any step, revert the policy or the lambda. This is a configuration change only.

Without an Enterprise Plan#

On other paid plans, risk signals are calculated only when a risk policy is enabled, and the policy enforces immediately. There is no shadow mode, so roll out small and reversibly.

  1. Record baseline data. Record your current challenge rate from the Reactor tab counts.
  2. Pilot risk based MFA. Set "Challenge On High Risk" on a dedicated low-traffic or internal tenant.
  3. Measure the impact. Compare the Reactor counts for this tenant to your baseline.
  4. Widen the rollout. Enable the policy on additional tenants, one at a time.

To roll back, set the policy to your previous MFA policy.

Example MFA Requirement Lambdas#

The following example MFA requirement lambda mimics the Disabled tenant policy:

function checkRequired(result, user, registration, context) {
  result.required = false;
}

This lambda mimics the Enabled tenant policy:

function checkRequired(result, user, registration, context) {
  var methods = (user.twoFactor && user.twoFactor.methods) || [];
  result.required = methods.length > 0;
}

The following lambda mimics the Required tenant policy:

function checkRequired(result, user, registration, context) {
  result.required = true;
}

The following lambda mimics an application policy:

// Returns true when an existing two-factor trust should bypass the challenge,
// per the application's applicationMultiFactorTrustPolicy.
function hasValidTrust(context) {
  // Default to 'Any' to match FusionAuth's default trust behavior when unset.
  var trustPolicy = (context.policies && context.policies.applicationMultiFactorTrustPolicy) || 'Any';

  // 'None' never honors an existing trust.
  if (trustPolicy === 'None') {
    return false;
  }

  var trust = context.mfaTrust;
  if (!trust) {
    return false;
  }

  // Reject an expired trust.
  if (trust.expirationInstant && trust.expirationInstant <= Date.now()) {
    return false;
  }

  // 'This' only honors a trust established by the current application.
  if (trustPolicy === 'This') {
    var currentAppId = context.application && context.application.id;
    return !!currentAppId && trust.applicationId === currentAppId;
  }

  // 'Any' honors a valid trust from any application.
  return true;
}

function checkRequired(result, user, registration, context) {
  var policies = context.policies || {};

  // Effective login policy: the application overrides the tenant when set.
  var loginPolicy = policies.applicationLoginPolicy || policies.tenantLoginPolicy;

  // Does the user have a configured MFA method?
  var methods = (user.twoFactor && user.twoFactor.methods) || [];
  var hasMethod = methods.length > 0;

  // Base requirement from the login policy.
  var required;
  if (loginPolicy === 'Required') {
    required = true;       // always challenge; FusionAuth forces enrollment if no method exists
  } else if (loginPolicy === 'Enabled') {
    required = hasMethod;  // challenge only when the user has a method
  } else {
    required = false;      // Disabled, or no policy set
  }

  // Honor the application trust policy. An existing trust only matters
  // when a challenge would otherwise occur.
  if (required && hasValidTrust(context)) {
    required = false;
  }

  result.required = required;
}

Custom MFA Logic#

You may need more granularity on who is challenged for an additional factor during login. For example, you might want everyone who is a member of a certain group or has a certain user.data field to complete an MFA challenge.

To customize MFA to meet your needs, use one of the following methods:

MFA Requirement Lambda#

Under Customization -> Lambdas , create a new Lambda of type MFA requirement lambda.

The following example forces an MFA check for any user whose email address includes the string 'gilfoyle':

function checkRequired(result, user, registration, context) {
  if (user.email.includes('gilfoyle')) {
    result.required = true;
  }
}

For more information, see the MFA requirement lambda documentation.

Use a Webhook#

  1. Set a tenant or application policy to Enabled and then use the API to ensure that everyone in the mfa_required group has an MFA method.
  2. Use a transactional user.update webhook to ensure that the MFA method can't be removed while the user is in that group.
  3. Set the trust duration to be the same or less than your session length.
  4. If you are using the application policy, set the trust policy to This.

Redirect Users Through a Special Application#

The following example guarantees that all users in the mfa_required group are challenged with MFA whenever they log in:

  1. Create an "MFA check" application with an MFA policy of Required and a trust policy of None.
  2. After a user logs in, examine their profile on an interstitial page.
    1. If they are in the mfa_required group, redirect them to this application. They will be prompted for MFA and then redirected to the initial application.
    2. Users that are not in the mfa_required group can be sent directly through to the application.

Make sure the MFA check application uses a lambda to set the aud and applicationId claims to values expected by any access token consumers.

This is similar to doing a conditional step-up authentication on the interstitial page, without using the step-up API.

MFA Challenges Outside Of The Login Process#

If you need to prompt for MFA outside of the login process, use step-up authentication. For example, you could display an MFA challenge when a user performs a high-risk action like initiating a money transfer. Use the 2FA API to perform this process.

For more information, see the step-up authentication documentation.

Limitations On MFA Challenges#

There's an open issue about unexpected MFA behavior and workarounds when a user logs in with an identity provider but SSOs to an application with a login policy of Required.

If a user signs up with an MFA method that is allowed for a tenant, such as email, and the tenant configuration changes later to disable that MFA method, the user can still use that MFA method. Users cannot, however, add disabled MFA methods. If you are disabling an MFA method previously in use, it's recommended you search for users using that method and remove it using a script, updating each user.