Enabling Email as an MFA method

Hi, I am looking for an answer about how to allow our users to select between OTP and email during the MFA challenge. I have been back and forth on this with Auth0 support, and have utilized AI tools as well. I am 99.999% sure that I have configured things correctly, but no matter what I do, email does not get recognized as a valid mfa method. My users are email_verified, and have an email address, and therefore are enrolled.

The kink in this setup is this, and it probably makes a difference:

We are acting as IDP in our application. A user goes to our app and logs in. if they are authenticated, we set a partially authenticated state and redirect the user to Auth0, with a SAML Assertion that authenticates them with Auth0. If their user does not exist in Auth0, they are created on the fly. An Action then challenges them for their MFA. If they are a new user, they get properly challenged for a OTP (and if SMS is enabled they get a link to try that instead). If they pass, they are redirected back to our app with a SAML Assertion that validates that they have passed the MFA, and we clear the state flag in our application and allow them in as fully authenticated.

My understanding is, that on subsequent MFA challenges, they should have the option of choosing email, but that link never appears. I can interrogate all sorts of things in the action indicating that the user has email_verified, and that email is a valid authentication method for them, but when it comes to actually exercising the email MFA, if the action allows it, they are deferred to OTP, and if the action does not allow it, they get an error:

Unknown multifactor provider undefined

I am pretty sure this is a question that can only be answered by a developer who knows the definitive conditions under which the alternate mfa link would be displayed. Please understand that I have configured everything multiple times with multiple permutations, with the direct guidance of Auith0 support, so unless there is some subtle setting I am missing, it is not necessary to paste the standard email mfa config or Action script here. However, if you have experience with this kind of setup, and/or a pertinent piece of information, I would really appreciate knowing it.

Thanks!

Hi @cbuxbaum

Welcome to the Auth0 Community!

Please allow me some time to research the matter and I will return with an update for you.

Best regards,
Gerald

Is your relaying party a custom built app of yours? Are you sending the MFA challenge type in the request?

Hi Jonathan,

I’m not utilizing the API. We are doing IDP initiated SAML, with our app acting as the IDP. Auth0 is the Service Provider, and because we have a signed SAML assertion, we are able to authenticate on the fly without an Auth0 login screen. However, we have set up to always challenge MFA, in a post-login Action, and that is where Auth0 should be throwing up the factor picker to let us choose between OTP and email, which it never does, and errors with “Unknown multifactor provider undefined” if we try to force an email challenge in the action.

Using

api.multifactor.enable(‘any’, { allowRememberBrowser: false });

works fine to challenge with otp or phone, but never gives us the opportunity to switch between factors.

Hi @cbuxbaum

One detail stood out to me, which is that you’ve mentioned using api.multifactor.enable(‘any’, { allowRememberBrowser: false }); in order to accomplish your goal. This object will only instruct the flow to enable and require MFA from the user authenticating during that flow and my recommendation for a more suitable option would be to test using api.authentication.challengeWithAny(factors), please see this documentation, as it’s designed to present the user with a list of MFA factors from which they can choose. Another option worth testing out would be api.authentication.challengeWith([{ type: 'factor1'}, { type: 'factor2' }]} depending on your integration, one might work while the other might fail.

I hope my understanding was correct based on the initial context and your previous reply, but let me know if this worked better for you. In case you are still seeing the same behaviour, please test out without an Action presenting the MFA picker, but instead by making the following changes:

  • in your tenant, navigate to Security → Multi-factor Auth;
  • enable OTP and Email as factors;
  • select Always require MFA;
  • under Additional settings, toggle on " Show multi-factor Authentication Options";
  • ensure factors such as Actions do not interfere with the Authentication flow.

Let us know if in your tests you are able to get the correct behaviour using one of the above methods. Also, thank you for also reaching out to Auth0 Support via a case for this!

Best regards,
Gerald

Hi Gerald, thank you for taking the time for this. I have tried these specific challenges, and they frankly don’t work in the context of what we are doing, perhaps because we don’t utilize username/password authentication into Auth0. I still think there is a bug when doing SAML with Auth0 as the SP, that results in the error page

Unknown multifactor provider undefined

whenever we try to call out specific factors for a challenge or enrollment.

However, I will detail our workaround below, utilizing forms as suggested by Auth0 for the email challenge, and still maintaining

api.multifactor.enable(‘any’, { allowRememberBrowser: false });

in the action for otp enrollment and challenge.

Replying to my own question with our workaround, in case it helps anyone else.

Here is our action:

exports.onExecutePostLogin = async (event, api) => {

  api.samlResponse.setAttribute('mfa_passed', 'false')

const enrolledFactors = event.user.enrolledFactors;

if (enrolledFactors && enrolledFactors.length > 0) {

console.log('User Enrolled Factors:', JSON.stringify(enrolledFactors, null, 2));

const isOtpEnrolled = enrolledFactors.some(factor => factor.type === 'otp');

if (!isOtpEnrolled) {

api.multifactor.enable('any', { allowRememberBrowser: false });

api.transaction.setMetadata('mfa_complete', 'true');

return;

}

}

if (event.user.mfa_email === 'true')

{

api.prompt.render('ap_8cDH62MbNTAFGP2b5GpZLt');

if (event.prompt != undefined && event.prompt.vars != undefined)

{

api.samlResponse.setAttribute('mfa_passed', event.prompt.vars.mfa_complete)

console.log(event.prompt.vars.mfa_complete)

}

}

else{

api.multifactor.enable('any', { allowRememberBrowser: false });

}

}




/**

* @param {Event} event - Details about the user and the context in which they are logging in.

* @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login.

*/

exports.onContinuePostLogin = async (event, api) => {

if (event.user.mfa_email === 'true')

{

if (event.prompt != undefined && event.prompt.vars != undefined && event.prompt.vars.mfa_complete !== 'true' && event.transaction?.metadata?.mfa_complete !== 'true')

{

console.log(event.prompt.vars.mfa_complete);

api.multifactor.enable('any', { allowRememberBrowser: false });

}

else{

return;

}

}

}

and here is the exported json for our form which we started from the email otp template

{
  "version": "4.0.0",
  "form": {
    "name": "Email OTP verification with Auth0 Email Provider",
    "languages": {
      "primary": "en"
    },
    "nodes": [
      {
        "id": "step_qJEB",
        "type": "STEP",
        "coordinates": {
          "x": 599,
          "y": -615
        },
        "alias": "Email verification",
        "config": {
          "components": [
            {
              "id": "rich_text_Cqas",
              "category": "BLOCK",
              "type": "RICH_TEXT",
              "config": {
                "content": "<h2>Check your email</h2><p>Please enter the 6-digit verification code that was sent to <strong>{{context.user.email}}</strong>. The code is valid for 5 minutes.</p>"
              }
            },
            {
              "id": "verification_code",
              "category": "FIELD",
              "type": "NUMBER",
              "label": "Verification code",
              "required": true,
              "sensitive": false
            },
            {
              "id": "next_button_BtEe",
              "category": "BLOCK",
              "type": "NEXT_BUTTON",
              "config": {
                "text": "Continue"
              }
            },
            {
              "id": "resend_button_7eih",
              "category": "BLOCK",
              "type": "RESEND_BUTTON",
              "config": {
                "active_text": "Didn't receive the code?",
                "button_text": "Resend",
                "waiting_text": "Resend in {{remaining_seconds}} seconds",
                "text_alignment": "CENTER",
                "flow_id": "#FLOW-1#",
                "max_attempts": 3,
                "waiting_time": 60
              }
            }
          ],
          "next_node": "flow_hq3b"
        }
      },
      {
        "id": "flow_hq3b",
        "type": "FLOW",
        "coordinates": {
          "x": 1202,
          "y": -204
        },
        "config": {
          "flow_id": "#FLOW-2#",
          "next_node": "$ending"
        }
      },
      {
        "id": "flow_3Qle",
        "type": "FLOW",
        "coordinates": {
          "x": 337,
          "y": -50
        },
        "config": {
          "flow_id": "#FLOW-1#",
          "next_node": "step_qJEB"
        }
      },
      {
        "id": "step_5cJB",
        "type": "STEP",
        "coordinates": {
          "x": 169,
          "y": 558
        },
        "alias": "New step",
        "config": {
          "components": [
            {
              "id": "choice_GAHd",
              "category": "FIELD",
              "type": "CHOICE",
              "label": "MFA Factor",
              "required": false,
              "sensitive": false,
              "config": {
                "multiple": false,
                "options": [
                  {
                    "label": "OTP Authenticator",
                    "value": "otp"
                  },
                  {
                    "label": "Email",
                    "value": "email"
                  }
                ]
              }
            },
            {
              "id": "next_button_xA1f",
              "category": "BLOCK",
              "type": "NEXT_BUTTON",
              "config": {
                "text": "Continue"
              }
            }
          ],
          "next_node": "router_5UHb"
        }
      },
      {
        "id": "router_5UHb",
        "type": "ROUTER",
        "coordinates": {
          "x": 734,
          "y": 346
        },
        "alias": "Choose  MFA Factor",
        "config": {
          "rules": [
            {
              "id": "id_5853828829579",
              "alias": "Email",
              "condition": {
                "operands": [
                  {
                    "operands": [
                      "{{fields.choice_GAHd}}",
                      "email"
                    ],
                    "operator": "EQ"
                  }
                ],
                "operator": "AND"
              },
              "next_node": "flow_3Qle"
            },
            {
              "id": "id_9874204690555",
              "alias": "Otp",
              "condition": {
                "operands": [
                  {
                    "operands": [
                      "{{fields.choice_GAHd}}",
                      "otp"
                    ],
                    "operator": "EQ"
                  }
                ],
                "operator": "AND"
              },
              "next_node": "flow_2V1u"
            }
          ]
        }
      },
      {
        "id": "flow_2V1u",
        "type": "FLOW",
        "coordinates": {
          "x": 1203,
          "y": 255
        },
        "alias": "New flow",
        "config": {
          "flow_id": "#FLOW-3#",
          "next_node": "$ending"
        }
      }
    ],
    "start": {
      "next_node": "step_5cJB",
      "coordinates": {
        "x": -197,
        "y": 41
      }
    },
    "ending": {
      "resume_flow": true,
      "coordinates": {
        "x": 1709,
        "y": -205
      }
    }
  },
  "flows": {
    "#FLOW-1#": {
      "name": "Generate OTP & Send email",
      "actions": [
        {
          "id": "generate_otp",
          "type": "OTP",
          "action": "GENERATE_CODE",
          "allow_failure": false,
          "mask_output": false,
          "params": {
            "reference": "{{context.user.email}}",
            "length": 6
          }
        },
        {
          "id": "send_email",
          "type": "AUTH0",
          "action": "SEND_EMAIL",
          "allow_failure": false,
          "mask_output": false,
          "params": {
            "to": "{{context.user.email}}",
            "subject": "{{custom_vars.code}} is your verification code",
            "body": "{% if user.nickname %}\n<p>Hello {{user.nickname}},</p>\n{% else %}\n<p>Hello,</p>\n{% endif %}\n<p>{{custom_vars.code}} is your verification code</p>",
            "custom_vars": {
              "code": "{{actions.generate_otp.code}}"
            }
          }
        }
      ]
    },
    "#FLOW-2#": {
      "name": "Verify OTP & Update user",
      "actions": [
        {
          "id": "verify_otp",
          "type": "OTP",
          "action": "VERIFY_CODE",
          "allow_failure": false,
          "mask_output": false,
          "params": {
            "reference": "{{context.user.email}}",
            "code": "{{fields.verification_code}}"
          }
        },
        {
          "id": "if_then_condition_mnXT",
          "alias": "Is it valid?",
          "type": "FLOW",
          "action": "BOOLEAN_CONDITION",
          "allow_failure": false,
          "mask_output": false,
          "params": {
            "if": {
              "operands": [
                {
                  "operands": [
                    "{{actions.verify_otp.valid}}",
                    true
                  ],
                  "operator": "EQ"
                }
              ],
              "operator": "AND"
            },
            "then": [
              {
                "id": "update_user_IPZP",
                "type": "AUTH0",
                "action": "UPDATE_USER",
                "allow_failure": false,
                "mask_output": false,
                "params": {
                  "connection_id": "#CONN-1#",
                  "user_id": "{{context.user.user_id}}",
                  "changes": {
                    "email_verified": true
                  }
                }
              },
              {
                "id": "store_shared_variable_ggJG",
                "alias": "mfa_complete",
                "type": "FLOW",
                "action": "STORE_VARS",
                "allow_failure": false,
                "mask_output": false,
                "params": {
                  "vars": {
                    "mfa_complete": "true"
                  }
                }
              }
            ],
            "else": [
              {
                "id": "show_error_message_Whan",
                "type": "FLOW",
                "action": "ERROR_MESSAGE",
                "allow_failure": false,
                "mask_output": false,
                "params": {
                  "message": "We're sorry, your verification code is not valid. Please, review it and try it again."
                }
              }
            ]
          }
        }
      ]
    },
    "#FLOW-3#": {
      "name": "check choice",
      "actions": [
        {
          "id": "if_then_condition_alzN",
          "alias": "check choice",
          "type": "FLOW",
          "action": "BOOLEAN_CONDITION",
          "allow_failure": false,
          "mask_output": false,
          "params": {
            "if": {
              "operands": [
                {
                  "operands": [
                    "{{fields.choice_GAHd}}",
                    "otp"
                  ],
                  "operator": "EQ"
                }
              ],
              "operator": "AND"
            },
            "then": [
              {
                "id": "store_shared_variable_k9gD",
                "alias": "mfa_complete",
                "type": "FLOW",
                "action": "STORE_VARS",
                "allow_failure": false,
                "mask_output": false,
                "params": {
                  "vars": {
                    "mfa_complete": "false"
                  }
                }
              }
            ],
            "else": [
              {
                "id": "store_shared_variable_wxGS",
                "alias": "mfa_complete",
                "type": "FLOW",
                "action": "STORE_VARS",
                "allow_failure": false,
                "mask_output": false,
                "params": {
                  "vars": {
                    "mfa_complete": "true"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  },
  "connections": {
    "#CONN-1#": {
      "id": "ac_eXPm5kRYH1GtndQA84jrET",
      "app_id": "AUTH0",
      "name": "REPLACE_WITH_M2M_CONNECTION"
    }
  }
}

Hi everyone.

I will be providing more details on the matter since the topic remained unsolved in order to optimize the content on the Community forum and in case anybody else experiences a similar issue.

I understand that you are using Auth0 as an identity provider in a SAML assertion flow where your application acts as the IdP. Users authenticate to your application, are redirected to Auth0 with a SAML assertion, and then challenged for MFA via an Action. You want users to be able to select between OTP and email during the MFA challenge, but the email option never appears. When you attempt to challenge with email, you receive an "Unknown multifactor provider undefined" error.

After extensive troubleshooting with Auth0 support, you have discovered that the standard api.authentication.challengeWithAny() method does not work in SAML assertion flows. The solution is to use a custom prompt with api.prompt.render() to display a form that allows users to select their MFA method, combined with careful enrollment state checking and SAML response attribute handling.

[Root Cause]

Email MFA in Auth0 is classified as a "dependent" factor (not a true independent MFA method). In standard OAuth/OIDC flows, the challengeWithAny() API can display email as an option if the user is enrolled. However, in SAML assertion flows, this API does not properly recognize email as an enrolled method, resulting in the "Unknown multifactor provider undefined" error.

The issue is compounded by:

  1. SAML assertion context limitations — The MFA challenge flow in SAML assertion scenarios does not have the same enrollment metadata as standard OAuth/OIDC flows
  2. Email enrollment not recognized — Even if email_verified = true, the email factor must be explicitly enrolled, and this enrollment state is not always recognized in the SAML context
  3. Standard MFA APIs don't workapi.multifactor.enable('any') and api.authentication.challengeWithAny() are designed for standard login flows, not federated SAML assertion flows
  4. Custom prompts bypass the limitation — By using api.prompt.render() with a custom form, you can manually present the MFA options and handle the selection logic yourself

[Official Workaround]

The officially recommended solution is to use a custom prompt form that you control, rather than relying on the built-in challengeWithAny() API. This approach allows you to:

  1. Check the user's enrolled factors — Inspect event.user.enrolledFactors to determine which MFA methods the user has available
  2. Render a custom form — Use api.prompt.render() to display a form where users can select their preferred MFA method
  3. Handle MFA completion — Use onContinuePostLogin to process the user's selection and complete the MFA challenge
  4. Communicate MFA status — Set SAML response attributes to indicate MFA completion back to your application

[Solution]

Step 1: Create a custom prompt form

In the Auth0 Dashboard, navigate to Branding → Prompts and create a new custom form based on the Email OTP template. This form should:

  • Display a list of available MFA methods (OTP, Email, SMS, etc.)
  • Allow users to select their preferred method
  • Collect the MFA code after the user selects a method
  • Return the completion status via event.prompt.vars

Your custom form JSON (as you have already created) should include:

  • A selection interface for MFA methods
  • An input field for the verification code
  • Proper error handling and retry logic
  • Clear messaging about which method the user selected

Step 2: Implement the Post-Login Action with enrollment checking

Use the following Action structure to check enrolled factors and render your custom form:

exports.onExecutePostLogin = async (event, api) => {
  // Initialize SAML response attribute
  api.samlResponse.setAttribute('mfa_passed', 'false');

  // Get the user's enrolled factors
  const enrolledFactors = event.user.enrolledFactors;

  if (enrolledFactors && enrolledFactors.length > 0) {
    console.log('User Enrolled Factors:', JSON.stringify(enrolledFactors, null, 2));

    // Check if user has OTP enrolled (independent factor)
    const isOtpEnrolled = enrolledFactors.some(factor => factor.type === 'otp');

    // If OTP is NOT enrolled, enable MFA enrollment
    if (!isOtpEnrolled) {
      api.multifactor.enable('any', { allowRememberBrowser: false });
      api.transaction.setMetadata('mfa_complete', 'true');
      return;
    }
  }

  // If user has email MFA enabled in metadata, render custom form
  if (event.user.user_metadata && event.user.user_metadata.mfa_email === 'true') {
    // Render your custom prompt form (replace with your form ID)
    api.prompt.render('ap_YOUR_FORM_ID');

    // Check if form was completed
    if (event.prompt && event.prompt.vars) {
      api.samlResponse.setAttribute('mfa_passed', event.prompt.vars.mfa_complete);
      console.log('MFA Complete:', event.prompt.vars.mfa_complete);
    }
  } else {
    // Fallback: enable standard MFA if custom form not applicable
    api.multifactor.enable('any', { allowRememberBrowser: false });
  }
};

Step 3: Implement the Continue Post-Login Action

Handle the continuation of the login flow after the custom form is submitted:

exports.onContinuePostLogin = async (event, api) => {
  // Check if user has email MFA enabled
  if (event.user.user_metadata && event.user.user_metadata.mfa_email === 'true') {
    // Verify that MFA was completed
    if (event.prompt && event.prompt.vars && event.prompt.vars.mfa_complete !== 'true' && 
        event.transaction?.metadata?.mfa_complete !== 'true') {
      console.log('MFA not yet complete:', event.prompt.vars.mfa_complete);
      
      // Re-render the form if MFA is not complete
      api.multifactor.enable('any', { allowRememberBrowser: false });
    } else {
      // MFA is complete; allow login to proceed
      console.log('MFA completed successfully');
      return;
    }
  }
};

Step 4: Set user metadata to enable email MFA

For users who should have email MFA available, set the mfa_email flag in their user metadata:

// Via Management API or in your application logic
PATCH /api/v2/users/{user_id}
{
  "user_metadata": {
    "mfa_email": "true"
  }
}

Alternatively, set this during user creation or enrollment:

// In your application when creating the user
const user = await managementClient.users.create({
  email: userEmail,
  email_verified: true,
  user_metadata: {
    mfa_email: 'true'
  }
});

Step 5: Verify the custom form is properly configured

Ensure your custom form:

  1. Accepts user input — Allows users to select between OTP and Email
  2. Sends verification codes — Integrates with Auth0's email service to send OTP codes
  3. Returns completion status — Sets event.prompt.vars.mfa_complete = 'true' when MFA is successfully verified
  4. Handles errors gracefully — Shows appropriate error messages if verification fails

Your custom form JSON should include:

  • A dropdown or button selection for MFA method
  • An input field for the 6-digit verification code
  • Validation logic to ensure the code is correct
  • A success message and redirect after verification

Step 6: Test the flow end-to-end

  1. First login — User authenticates to your application, is redirected to Auth0 with SAML assertion
  2. MFA enrollment — If user is new, they are prompted to enroll in OTP (independent factor)
  3. Subsequent logins — User is presented with your custom form showing OTP and Email options
  4. Email selection — User selects Email, receives code, enters code, and completes MFA
  5. SAML response — Auth0 returns SAML assertion with mfa_passed=true attribute
  6. Application redirect — Your application receives the assertion and allows the user in

Why This Works:

The custom prompt approach works because:

  1. You control the UI — Instead of relying on Auth0's built-in MFA challenge UI (which doesn't recognize email in SAML flows), you create your own form
  2. Enrollment state is explicit — You check event.user.enrolledFactors directly and make decisions based on what you find
  3. SAML context is preserved — The custom form operates within the SAML assertion flow without losing context
  4. Email is treated as a valid option — Since you're not using challengeWithAny(), the "Unknown multifactor provider undefined" error is avoided

Why challengeWithAny() Fails in SAML Flows:

The challengeWithAny() API is designed for standard OAuth/OIDC flows where Auth0 has full control over the authentication context. In SAML assertion flows:

  1. Auth0 is acting as a Service Provider — Your application is the IdP, and Auth0 is receiving a pre-authenticated SAML assertion
  2. Enrollment metadata is incomplete — Auth0 does not have the same enrollment context as it would in a standard login flow
  3. Email factor is not recognized — The API cannot find email in the available factors because the SAML context doesn't include the necessary enrollment information
  4. The API falls back to OTP — Since email is not recognized, challengeWithAny() defaults to OTP or throws an error

We hope this resolves your email MFA selection issue. Your workaround is solid and represents the best practice for this scenario. If you encounter any issues implementing this approach, please contact Auth0 Support with your Action code and custom form configuration.