> ## Documentation Index
> Fetch the complete documentation index at: https://stytch.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding MFA to a custom auth flow

> Integrate MFA with Stytch's headless or backend SDKs for complete control over your authentication flow.

## Prerequisites

Before you can add MFA to a custom auth flow, you'll need to complete the following steps:

<Steps>
  <Step title="Build out primary authentication">
    Before integrating MFA, you need to already have a **primary authentication flow** built out. You can do so by following one of the integration guides:

    <CardGroup cols={3}>
      <Card title="Email Magic Links" icon="mail-check" href="/multi-tenant-auth/authentication/magic-links/overview" />

      <Card title="OAuth" icon="shield-check" href="/multi-tenant-auth/authentication/oauth/overview" />

      <Card title="SSO" icon="building" href="/multi-tenant-auth/authentication/sso/overview" />
    </CardGroup>
  </Step>

  <Step title="Configure enforced MFA for an Organization">
    To configure MFA, you'll need to first toggle on "Require MFA" for at least one Organization in the [Stytch Dashboard](https://stytch.com/dashboard/organizations), or call the [Update Organization API](/api-reference/b2b/api/organizations/update-organization) with `mfa_policy` set to `REQUIRED_FOR_ALL`.

    Each Organization can specify which `mfa_methods` are allowed for Members in their Organization: either `ALL_ALLOWED` or `RESTRICTED`. If `RESTRICTED`, Members can only use MFA methods specified in the `allowed_mfa_methods` array.

    For example, if an Organization requires Members to use TOTP MFA:

    ```json theme={null}
    {
        "mfa_policy": "REQUIRED_FOR_ALL",
        "mfa_methods": "RESTRICTED",
        // Optional, not enforced if mfa_methods is ALL_ALLOWED
        "allowed_mfa_methods": ["totp"]
    }
    ```
  </Step>
</Steps>

## Integrating MFA

Stytch's headless and backend SDKs provide a flexible way to integrate MFA into your authentication flow.

<Tabs>
  <Tab title="Headless frontend SDK">
    <Tabs>
      <Tab title="First-time enrollment">
        ### Required MFA enrollment

        When an Organization's MFA policy is `REQUIRED_FOR_ALL` but the Member is not currently enrolled in MFA, you'll need to handle MFA enrollment after primary authentication.

        <Steps>
          <Step title="Detect when MFA enrollment required">
            The following authentication methods will return a Member Session if the Member is not enrolled in MFA OR an `intermediate_session_token` and an `mfa_required` object that indicates MFA is required in order to be granted a Session for the Organization:

            * [sso.authenticate()](/api-reference/b2b/frontend-sdks/react/methods/sso/authenticate)
            * [oauth.authenticate()](/api-reference/b2b/frontend-sdks/react/methods/oauth/discovery-authenticate)
            * [magicLinks.authenticate()](/api-reference/b2b/frontend-sdks/react/methods/email-magic-links/authenticate-discovery-magic-link)
            * [passwords.authenticate()](/api-reference/b2b/frontend-sdks/react/methods/passwords/authenticate)
            * [discovery.intermediateSessions.exchange()](/api-reference/b2b/frontend-sdks/react/methods/discovery/exchange-intermediate-session)
            * [passwords.resetByEmail()](/api-reference/b2b/frontend-sdks/react/methods/passwords/reset-by-email)
            * [passwords.resetByExistingPassword()](/api-reference/b2b/frontend-sdks/react/methods/passwords/reset-by-existing-password)

            If the Member is not yet enrolled in MFA, the response will look as follows:

            ```json theme={null}
            {
                "intermediate_session_token": "oNJB3foIA79dn_uNVMNghG_MGkKSLHnR65NsKXv0gZzY",
                "mfa_required": {
                    "member_options": null,
                    "secondary_auth_initiated": null
                },
                "member_authenticated": false,
                "organization": {
                    "mfa_methods": "REQUIRED_FOR_ALL",
                    "allowed_mfa_methods": ["sms_otp", "totp"]
                }
                ...
            }
            ```

            Indicating that `member_authenticated: false` due to `mfa_required` -- but no `member_options` are present. The Stytch SDK will automatically store the `intermediate_session_token` and handle passing that with subsequent calls to tie the user's primary and secondary authentication together.

            For the primary authentication endpoint(s) you've already integrated add handling to detect when MFA enrollment is required by checking to see if the `organization.mfa_methods` is `ALL_ALLOWED` or `RESTRICTED`. If `RESTRICTED` use the `organization.allowed_mfa_methods` to surface only allowed MFA methods to the user.

            Check the `organization.allowed_mfa_methods` and based on the available options, prompt the user to enroll in MFA.
          </Step>

          <Step title="Start MFA enrollment">
            <Tabs>
              <Tab title="SMS OTP">
                For SMS MFA, prompt the user for their phone number and call the SMS Send method with the number. Make sure the `mfa_phone_number` is in the E.164 format, e.g. "+14155551234".

                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback, useState } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const SendOtp = () => {
                    const stytchClient = useStytchB2BClient();
                    const [phoneNumber, setPhoneNumber] = useState('');

                    const trigger = useCallback(() => {
                      stytchClient.otps.sms.send({
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c2cfb6e1bd7d',
                        mfa_phone_number: phoneNumber,
                      });
                    }, [stytchClient, phoneNumber]);

                    return (
                      <div>
                        <input
                          type="tel"
                          placeholder="Enter phone number"
                          value={phoneNumber}
                          onChange={(e) => setPhoneNumber(e.target.value)}
                        />
                        <button onClick={trigger}>Send OTP</button>
                      </div>
                    );
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <input type="tel" id="phone-input" placeholder="Enter phone number" />
                    <button onclick="sendOtp()" type="submit">Send OTP</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const sendOtp = (e) => {
                      e.preventDefault();
                      stytch.otps.sms.send({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        mfa_phone_number: document.getElementById('phone-input').value,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                For TOTP MFA, you'll first trigger the TOTP Create method, which will return a `qr_code` that you will surface to the end user to register their authenticator app.

                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const CreateTOTP = () => {
                    const stytchClient = useStytchB2BClient();

                    const trigger = useCallback(() => {
                      stytchClient.totp.create({ expiration_minutes: 60 });
                    }, [stytchClient]);

                    return <button onClick={trigger}>Create TOTP</button>;
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <button onclick="create()" type="submit">Create TOTP</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const create = (e) => {
                      e.preventDefault();
                      stytch.totp.create({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        expiration_minutes: 60,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>
          </Step>

          <Step title="Authenticate to complete enrollment">
            Prompt the user to input their OTP or TOTP code in order to complete the MFA enrollment and authenticate.

            <Tabs>
              <Tab title="SMS OTP">
                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback, useState } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const Authenticate = () => {
                    const stytch = useStytchB2BClient();
                    const [code, setCode] = useState('');

                    const authenticate = useCallback(
                      (e) => {
                        e.preventDefault();
                        stytch.otps.sms.authenticate({
                          member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                          organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                          code: code,
                          session_duration_minutes: 60,
                        });
                      },
                      [stytch, code],
                    );

                    const handleChange = useCallback((e) => {
                      setCode(e.target.value);
                    }, []);

                    return (
                      <form>
                        <label htmlFor="otp-input">Enter code</label>
                        <input id="otp-input" autoComplete="one-time-code" inputMode="numeric" pattern="[0-9]*" onChange={handleChange} />
                        <button onClick={authenticate} type="submit">
                          Submit
                        </button>
                      </form>
                    );
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <label for="otp-input">Enter code</label>
                    <input id="otp-input" autocomplete="one-time-code" inputtype="numeric" pattern="[0-9]*"></input>
                    <button onclick="authenticate()" type="submit">Submit</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const authenticate = (e) => {
                      e.preventDefault();
                      stytch.otps.sms.authenticate({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        code: document.getElementById('otp-input').value,
                        session_duration_minutes: 60,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback, useState } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const Authenticate = () => {
                    const stytchClient = useStytchB2BClient();
                    const [totpCode, setTotpCode] = useState('');

                    const authenticate = useCallback(
                      (e) => {
                        e.preventDefault();
                        stytchClient.totp.authenticate({ code: totpCode, session_duration_minutes: 60 });
                      },
                      [stytchClient, totpCode],
                    );

                    const handleChange = useCallback((e) => {
                      setTotpCode(e.target.value);
                    }, []);

                    return (
                      <form>
                        <label htmlFor="totp-input">Enter code</label>
                        <input id="totp-input" value={totpCode} onChange={handleChange} />
                        <button onClick={authenticate} type="submit">
                          Submit
                        </button>
                      </form>
                    );
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <label for="totp-input">Enter code</label>
                    <input id="totp-input"></input>
                    <button onclick="authenticate()" type="submit">Submit</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const authenticate = (e) => {
                      e.preventDefault();
                      stytch.totp.authenticate({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        code: document.getElementById('totp-input').value,
                        session_duration_minutes: 60,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>
          </Step>

          <Step title="Test it out">
            Run your application and attempt to login to your Organization with the `REQUIRED_FOR_ALL` MFA policy to test out required enrollment in MFA.
          </Step>
        </Steps>
      </Tab>

      <Tab title="Returning login">
        ### Returning MFA authentication

        The flow for returning MFA authentication with a headless frontend integration is quite similar to the last steps of enrolling a Member in MFA for the first time.

        <Steps>
          <Step title="Detect user's MFA options">
            When a Member is already enrolled in MFA, the response from the various `authenticate()` methods discussed in the "Required MFA Enrollment" section will include details on the MFA options they are enrolled in:

            ```json theme={null}
            {
                "intermediate_session_token": "oNJB3foIA79dn_uNVMNghG_MGkKSLHnR65NsKXv0gZzY",
                "mfa_required": {
                    "member_options": {
                        "mfa_phone": "+14151112222",
                        "totp_registration_id": "member-totp-test-41920359-8bbb-4fe8-8fa3-aaa83f35f02c"
                    },
                    "secondary_auth_initiated": "sms_otp"
                },
                "member_authenticated": false,
                ...
            }
            ```

            If a Member's `default_mfa_method` is SMS OTP, Stytch will automatically trigger the SMS Send when the user performs primary authentication for that Organization and will indicate this in the `secondary_auth_initiated` field.

            Check the `member_options` and surface the appropriate input to the Member to complete MFA.
          </Step>

          <Step title="Authenticate MFA">
            Similar to the last step of MFA enrollment, handle submission of the TOTP or OTP code in order to finish the MFA authentication and log the member in based on their default MFA method.

            <Tabs>
              <Tab title="SMS OTP">
                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback, useState } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const Authenticate = () => {
                    const stytch = useStytchB2BClient();
                    const [code, setCode] = useState('');

                    const authenticate = useCallback(
                      (e) => {
                        e.preventDefault();
                        stytch.otps.sms.authenticate({
                          member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                          organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                          code: code,
                          session_duration_minutes: 60,
                        });
                      },
                      [stytch, code],
                    );

                    const handleChange = useCallback((e) => {
                      setCode(e.target.value);
                    }, []);

                    return (
                      <form>
                        <label htmlFor="otp-input">Enter code</label>
                        <input id="otp-input" autoComplete="one-time-code" inputMode="numeric" pattern="[0-9]*" onChange={handleChange} />
                        <button onClick={authenticate} type="submit">
                          Submit
                        </button>
                      </form>
                    );
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <label for="otp-input">Enter code</label>
                    <input id="otp-input" autocomplete="one-time-code" inputtype="numeric" pattern="[0-9]*"></input>
                    <button onclick="authenticate()" type="submit">Submit</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const authenticate = (e) => {
                      e.preventDefault();
                      stytch.otps.sms.authenticate({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        code: document.getElementById('otp-input').value,
                        session_duration_minutes: 60,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                <CodeGroup>
                  ```jsx React icon=react theme={null}
                  import { useCallback, useState } from 'react';
                  import { useStytchB2BClient } from '@stytch/react/b2b';

                  export const Authenticate = () => {
                    const stytchClient = useStytchB2BClient();
                    const [totpCode, setTotpCode] = useState('');

                    const authenticate = useCallback(
                      (e) => {
                        e.preventDefault();
                        stytchClient.totp.authenticate({ code: totpCode, session_duration_minutes: 60 });
                      },
                      [stytchClient, totpCode],
                    );

                    const handleChange = useCallback((e) => {
                      setTotpCode(e.target.value);
                    }, []);

                    return (
                      <form>
                        <label htmlFor="totp-input">Enter code</label>
                        <input id="totp-input" value={totpCode} onChange={handleChange} />
                        <button onClick={authenticate} type="submit">
                          Submit
                        </button>
                      </form>
                    );
                  };
                  ```

                  ```html Vanilla JS icon=js theme={null}
                  <form>
                    <label for="totp-input">Enter code</label>
                    <input id="totp-input"></input>
                    <button onclick="authenticate()" type="submit">Submit</button>
                  </form>
                  <script>
                    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
                    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

                    const authenticate = (e) => {
                      e.preventDefault();
                      stytch.totp.authenticate({
                        member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                        organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                        code: document.getElementById('totp-input').value,
                        session_duration_minutes: 60,
                      });
                    };
                  </script>
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>
          </Step>

          <Step title="Test it out">
            Run your application and go through an authentication flow with a user who already has an MFA method enabled.
          </Step>
        </Steps>
      </Tab>

      <Tab title="Optional enrollment">
        ### Optional MFA enrollment

        If an Organization's MFA policy is `OPTIONAL`, members can still opt to secure their account with MFA. At a high level, the flow for this is very similar to required enrollment, but can occur anytime after the Member has logged in and has been granted a Session.

        The headless SDK will take care of including the `session_jwt` / `session_token` in the `authenticate()` calls, so the only additional piece you'd need to do is to pass in the `set_mfa_enrollment` flag:

        <Tabs>
          <Tab title="SMS OTP">
            <CodeGroup>
              ```jsx React icon=react theme={null}
              stytch.otps.sms.authenticate({
                member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                code: otpCode,
                session_duration_minutes: 60,
                set_mfa_enrollment: 'enroll'
              });
              ```

              ```javascript Vanilla JS icon=js theme={null}
              stytch.otps.sms.authenticate({
                member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                code: document.getElementById('otp-input').value,
                session_duration_minutes: 60,
                set_mfa_enrollment: 'enroll'
              });
              ```
            </CodeGroup>
          </Tab>

          <Tab title="TOTP">
            <CodeGroup>
              ```jsx React icon=react theme={null}
              stytch.totp.authenticate({
                member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                code: totpCode,
                session_duration_minutes: 60,
                set_mfa_enrollment: 'enroll'
              });
              ```

              ```javascript Vanilla JS icon=js theme={null}
              stytch.totp.authenticate({
                member_id: 'member-test-32fc5024-9c09-4da3-bd2e-c9ce4da9375f',
                organization_id: 'organization-test-07971b06-ac8b-4cdb-9c15-63b17e653931',
                code: document.getElementById('totp-input').value,
                session_duration_minutes: 60,
                set_mfa_enrollment: 'enroll'
              });
              ```
            </CodeGroup>
          </Tab>
        </Tabs>

        If the Member is enrolling in an additional form of MFA (e.g. has SMS MFA already, and is adding TOTP) you can also specify that the newly enrolled MFA method becomes the default through the `set_default_mfa` boolean.

        If you want to enroll the user in MFA for occasional step-up authentication on particularly sensitive actions, rather than MFA that is required on each login you can omit the `set_mfa_enrollment` field in the `authenticate()` call.
      </Tab>
    </Tabs>
  </Tab>

  <Tab title="Backend SDK">
    <Tabs>
      <Tab title="First-time enrollment">
        ### Required MFA enrollment

        When an Organization's MFA policy is `REQUIRED_FOR_ALL` but the Member is not currently enrolled in MFA, you'll need to handle MFA enrollment after primary authentication.

        <Steps>
          <Step title="Detect when MFA enrollment required">
            The following APIs will return a Member Session if the Member is not enrolled in MFA OR an `intermediate_session_token` and an `mfa_required` object that indicates MFA is required in order to be granted a Session for the Organization:

            * [Authenticate SSO](/api-reference/b2b/api/sso/shared/complete-sso-authenticate)
            * [Authenticate OAuth](/api-reference/b2b/api/oauth/discovery/authenticate)
            * [Authenticate Magic Link](/api-reference/b2b/api/email-magic-links/organization/authenticate-magic-link)
            * [Authenticate Password](/api-reference/b2b/api/passwords/authenticate)
            * [Exchange Session](/api-reference/b2b/api/sessions/exchange-session)
            * [Password Reset by Email](/api-reference/b2b/api/passwords/create-or-reset-options/password-reset-by-email)
            * [Password Reset by Existing Password](/api-reference/b2b/api/passwords/create-or-reset-options/password-reset-by-existing-password)

            If the Member is not yet enrolled in MFA, the response will look as follows:

            ```json theme={null}
            {
                "intermediate_session_token": "oNJB3foIA79dn_uNVMNghG_MGkKSLHnR65NsKXv0gZzY",
                "mfa_required": {
                    "member_options": null,
                    "secondary_auth_initiated": null
                },
                "member_authenticated": false,
                "organization": {
                    "mfa_methods": "REQUIRED_FOR_ALL",
                    "allowed_mfa_methods": ["sms_otp", "totp"]
                }
            }
            ```

            Indicating that `member_authenticated: false` due to `mfa_required` -- but no `member_options` are present.

            For the primary authentication endpoint(s) you've already integrated add handling to detect when MFA enrollment is required by checking to see if the `organization.mfa_methods` is `ALL_ALLOWED` or `RESTRICTED`. If `RESTRICTED` use the `organization.allowed_mfa_methods` to surface only allowed MFA methods to the user.

            <CodeGroup>
              ```python Python icon=python theme={null}
              @app.route("/authenticate", methods=["POST"])
              def authenticate():
                  # After primary authentication (e.g., magic link, password, OAuth)
                  resp = stytch_client.magic_links.authenticate(
                      magic_links_token=request.form.get("token")
                  )

                  if resp.member_authenticated:
                      # No MFA required, create session
                      session["stytch_session_jwt"] = resp.session_jwt
                      return redirect(url_for("dashboard"))

                  # MFA is required - store intermediate session token
                  session["intermediate_session_token"] = resp.intermediate_session_token

                  # Check allowed MFA methods
                  allowed_methods = resp.organization.allowed_mfa_methods
                  if resp.organization.mfa_methods == "RESTRICTED":
                      # Only show allowed methods to user
                      return render_template("mfa_enrollment.html", allowed_methods=allowed_methods)

                  return render_template("mfa_enrollment.html", allowed_methods=["sms_otp", "totp"])
              ```

              ```javascript Node.js icon=node theme={null}
              app.post("/authenticate", async (req, res) => {
                // After primary authentication (e.g., magic link, password, OAuth)
                const resp = await stytchClient.magicLinks.authenticate({
                  magic_links_token: req.body.token,
                });

                if (resp.member_authenticated) {
                  // No MFA required, create session
                  req.session.stytch_session_jwt = resp.session_jwt;
                  return res.redirect("/dashboard");
                }

                // MFA is required - store intermediate session token
                req.session.intermediate_session_token = resp.intermediate_session_token;

                // Check allowed MFA methods
                const allowedMethods = resp.organization.allowed_mfa_methods;
                if (resp.organization.mfa_methods === "RESTRICTED") {
                  // Only show allowed methods to user
                  return res.render("mfa_enrollment", { allowedMethods });
                }

                return res.render("mfa_enrollment", { allowedMethods: ["sms_otp", "totp"] });
              });
              ```
            </CodeGroup>

            Your frontend should prompt the user to enroll in MFA via the allowed methods.

            For TOTP, no user input is required to start registration. For SMS, your frontend will need to collect the user's phone number in E.164 format (e.g., "+14155551234").
          </Step>

          <Step title="Start MFA enrollment">
            Add routes to handle starting the MFA enrollment process for SMS OTP or TOTP, depending on the users' selection.

            <Tabs>
              <Tab title="SMS OTP">
                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/sms/send", methods=["POST"])
                  def mfa_sms_send():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      phone_number = request.form.get("phone_number")

                      resp = stytch_client.otps.sms.send(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          mfa_phone_number=phone_number,
                          intermediate_session_token=intermediate_session_token,
                      )

                      if resp.status_code != 200:
                          return "Error sending OTP", 500

                      return render_template("mfa_verify.html", mfa_type="sms")
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/sms/send", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const phoneNumber = req.body.phone_number;

                    const resp = await stytchClient.otps.sms.send({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      mfa_phone_number: phoneNumber,
                      intermediate_session_token: intermediateSessionToken,
                    });

                    return res.render("mfa_verify", { mfaType: "sms" });
                  });
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/totp/create", methods=["POST"])
                  def mfa_totp_create():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      resp = stytch_client.totps.create(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          intermediate_session_token=intermediate_session_token,
                          expiration_minutes=60,
                      )

                      if resp.status_code != 200:
                          return "Error creating TOTP", 500

                      # Return the QR code for the user to scan
                      return render_template(
                          "mfa_totp_setup.html",
                          qr_code=resp.qr_code,
                          secret=resp.secret,
                      )
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/totp/create", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const resp = await stytchClient.totps.create({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      intermediate_session_token: intermediateSessionToken,
                      expiration_minutes: 60,
                    });

                    // Return the QR code for the user to scan
                    return res.render("mfa_totp_setup", {
                      qrCode: resp.qr_code,
                      secret: resp.secret,
                    });
                  });
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>
          </Step>

          <Step title="Authenticate to complete enrollment">
            You'll want to add a route that allows the user to finish the enrollment process by authenticating with their chosen factor.

            <Tabs>
              <Tab title="SMS OTP">
                Your frontend should prompt the user for the 6-digit code they received via SMS and submit it along with:

                * `organization_id`: The Organization's ID
                * `member_id`: The Member's ID
                * `code`: The OTP code entered by the user
                * `intermediate_session_token`: The token from primary authentication

                When the user submits their OTP code, you'll call Stytch to authenticate the code. If successful, this will complete the Member's enrollment in MFA and grant them a Member Session.

                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/sms/authenticate", methods=["POST"])
                  def mfa_sms_authenticate():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      code = request.form.get("code")

                      resp = stytch_client.otps.sms.authenticate(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          code=code,
                          intermediate_session_token=intermediate_session_token,
                          session_duration_minutes=60,
                      )

                      if resp.status_code != 200:
                          return "Error authenticating OTP", 500

                      # Clear intermediate session and set authenticated session
                      session.pop("intermediate_session_token", None)
                      session["stytch_session_jwt"] = resp.session_jwt

                      return redirect(url_for("dashboard"))
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/sms/authenticate", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const code = req.body.code;

                    const resp = await stytchClient.otps.sms.authenticate({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      code: code,
                      intermediate_session_token: intermediateSessionToken,
                      session_duration_minutes: 60,
                    });

                    // Clear intermediate session and set authenticated session
                    delete req.session.intermediate_session_token;
                    req.session.stytch_session_jwt = resp.session_jwt;

                    return res.redirect("/dashboard");
                  });
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                For TOTP, you'll need to first surface the QR Code so the user can create a TOTP registration in their authenticator app. Once registered, your UI should prompt them to input the TOTP code from their authenticator app. Your frontend should submit:

                * `organization_id`: The Organization's ID
                * `member_id`: The Member's ID
                * `code`: The 6-digit TOTP code from their authenticator app
                * `intermediate_session_token`: The token from primary authentication

                When the user submits their TOTP code, you'll call Stytch to authenticate the code. If successful, this will complete the Member's enrollment in MFA and grant them a Member Session.

                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/totp/authenticate", methods=["POST"])
                  def mfa_totp_authenticate():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      code = request.form.get("code")

                      resp = stytch_client.totps.authenticate(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          code=code,
                          intermediate_session_token=intermediate_session_token,
                          session_duration_minutes=60,
                      )

                      if resp.status_code != 200:
                          return "Error authenticating TOTP", 500

                      # Clear intermediate session and set authenticated session
                      session.pop("intermediate_session_token", None)
                      session["stytch_session_jwt"] = resp.session_jwt

                      return redirect(url_for("dashboard"))
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/totp/authenticate", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const code = req.body.code;

                    const resp = await stytchClient.totps.authenticate({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      code: code,
                      intermediate_session_token: intermediateSessionToken,
                      session_duration_minutes: 60,
                    });

                    // Clear intermediate session and set authenticated session
                    delete req.session.intermediate_session_token;
                    req.session.stytch_session_jwt = resp.session_jwt;

                    return res.redirect("/dashboard");
                  });
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>

            The member is now enrolled in MFA, and will be prompted to perform MFA through their selected method on subsequent authentication.
          </Step>

          <Step title="Test it out">
            Run your application and attempt to login to your Organization with the `REQUIRED_FOR_ALL` MFA policy to test out required enrollment in MFA.
          </Step>
        </Steps>
      </Tab>

      <Tab title="Returning login">
        ### Returning MFA authentication

        The flow for returning MFA authentication with a backend integration is quite similar to the last step of enrolling a Member in MFA for the first time.

        <Steps>
          <Step title="Detect user's MFA options">
            When a Member is already enrolled in MFA, the response from the various `authenticate()` APIs discussed in the "Required MFA Enrollment" section will indicate this in the `mfa_required.member_options` object:

            ```json theme={null}
            {
                "intermediate_session_token": "oNJB3foIA79dn_uNVMNghG_MGkKSLHnR65NsKXv0gZzY",
                "mfa_required": {
                    "member_options": {
                        "mfa_phone": "+14151112222",
                        "totp_registration_id": "member-totp-test-41920359-8bbb-4fe8-8fa3-aaa83f35f02c"
                    },
                    "secondary_auth_initiated": "sms_otp"
                },
                "member_authenticated": false
            }
            ```

            If a Member's `default_mfa_method` is SMS OTP, Stytch will automatically trigger the SMS Send when the user performs primary authentication for that Organization and will indicate this in the `secondary_auth_initiated` field.

            Check the `member_options` and surface the appropriate input to the Member to complete MFA:

            <CodeGroup>
              ```python Python icon=python theme={null}
              @app.route("/authenticate", methods=["POST"])
              def authenticate():
                  # After primary authentication
                  resp = stytch_client.magic_links.authenticate(
                      magic_links_token=request.form.get("token")
                  )

                  if resp.member_authenticated:
                      session["stytch_session_jwt"] = resp.session_jwt
                      return redirect(url_for("dashboard"))

                  # MFA required - check member's enrolled methods
                  session["intermediate_session_token"] = resp.intermediate_session_token
                  member_options = resp.mfa_required.member_options

                  if member_options:
                      # Member is already enrolled in MFA
                      mfa_methods = []
                      if member_options.mfa_phone:
                          mfa_methods.append("sms_otp")
                      if member_options.totp_registration_id:
                          mfa_methods.append("totp")

                      # Check if SMS was auto-triggered
                      sms_auto_sent = resp.mfa_required.secondary_auth_initiated == "sms_otp"

                      return render_template(
                          "mfa_verify.html",
                          mfa_methods=mfa_methods,
                          sms_auto_sent=sms_auto_sent,
                          masked_phone=member_options.mfa_phone,
                      )

                  # Member needs to enroll in MFA
                  return render_template("mfa_enrollment.html")
              ```

              ```javascript Node.js icon=node theme={null}
              app.post("/authenticate", async (req, res) => {
                // After primary authentication
                const resp = await stytchClient.magicLinks.authenticate({
                  magic_links_token: req.body.token,
                });

                if (resp.member_authenticated) {
                  req.session.stytch_session_jwt = resp.session_jwt;
                  return res.redirect("/dashboard");
                }

                // MFA required - check member's enrolled methods
                req.session.intermediate_session_token = resp.intermediate_session_token;
                const memberOptions = resp.mfa_required.member_options;

                if (memberOptions) {
                  // Member is already enrolled in MFA
                  const mfaMethods = [];
                  if (memberOptions.mfa_phone) {
                    mfaMethods.push("sms_otp");
                  }
                  if (memberOptions.totp_registration_id) {
                    mfaMethods.push("totp");
                  }

                  // Check if SMS was auto-triggered
                  const smsAutoSent = resp.mfa_required.secondary_auth_initiated === "sms_otp";

                  return res.render("mfa_verify", {
                    mfaMethods,
                    smsAutoSent,
                    maskedPhone: memberOptions.mfa_phone,
                  });
                }

                // Member needs to enroll in MFA
                return res.render("mfa_enrollment");
              });
              ```
            </CodeGroup>

            <Tabs>
              <Tab title="SMS OTP">
                Your frontend should prompt the user to input the OTP they received and submit it with:

                * `organization_id`: The Organization's ID
                * `member_id`: The Member's ID
                * `code`: The 6-digit OTP code
                * `intermediate_session_token`: The token from primary authentication
              </Tab>

              <Tab title="TOTP">
                Your frontend should prompt the user to input their TOTP code from their authenticator app and submit it with:

                * `organization_id`: The Organization's ID
                * `member_id`: The Member's ID
                * `code`: The 6-digit TOTP code
                * `intermediate_session_token`: The token from primary authentication
              </Tab>
            </Tabs>
          </Step>

          <Step title="Authenticate MFA">
            Similar to the last step of MFA enrollment, handle submission of the TOTP or OTP code in order to finish the MFA authentication and log the member in based on their default MFA method.

            <Tabs>
              <Tab title="SMS OTP">
                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/sms/authenticate", methods=["POST"])
                  def mfa_sms_authenticate():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      code = request.form.get("code")

                      resp = stytch_client.otps.sms.authenticate(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          code=code,
                          intermediate_session_token=intermediate_session_token,
                          session_duration_minutes=60,
                      )

                      if resp.status_code != 200:
                          return "Error authenticating OTP", 500

                      session.pop("intermediate_session_token", None)
                      session["stytch_session_jwt"] = resp.session_jwt

                      return redirect(url_for("dashboard"))
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/sms/authenticate", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const code = req.body.code;

                    const resp = await stytchClient.otps.sms.authenticate({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      code: code,
                      intermediate_session_token: intermediateSessionToken,
                      session_duration_minutes: 60,
                    });

                    delete req.session.intermediate_session_token;
                    req.session.stytch_session_jwt = resp.session_jwt;

                    return res.redirect("/dashboard");
                  });
                  ```
                </CodeGroup>
              </Tab>

              <Tab title="TOTP">
                <CodeGroup>
                  ```python Python icon=python theme={null}
                  @app.route("/mfa/totp/authenticate", methods=["POST"])
                  def mfa_totp_authenticate():
                      intermediate_session_token = session.get("intermediate_session_token")
                      if not intermediate_session_token:
                          return "No intermediate session", 400

                      code = request.form.get("code")

                      resp = stytch_client.totps.authenticate(
                          organization_id=session.get("organization_id"),
                          member_id=session.get("member_id"),
                          code=code,
                          intermediate_session_token=intermediate_session_token,
                          session_duration_minutes=60,
                      )

                      if resp.status_code != 200:
                          return "Error authenticating TOTP", 500

                      session.pop("intermediate_session_token", None)
                      session["stytch_session_jwt"] = resp.session_jwt

                      return redirect(url_for("dashboard"))
                  ```

                  ```javascript Node.js icon=node theme={null}
                  app.post("/mfa/totp/authenticate", async (req, res) => {
                    const intermediateSessionToken = req.session.intermediate_session_token;
                    if (!intermediateSessionToken) {
                      return res.status(400).send("No intermediate session");
                    }

                    const code = req.body.code;

                    const resp = await stytchClient.totps.authenticate({
                      organization_id: req.session.organization_id,
                      member_id: req.session.member_id,
                      code: code,
                      intermediate_session_token: intermediateSessionToken,
                      session_duration_minutes: 60,
                    });

                    delete req.session.intermediate_session_token;
                    req.session.stytch_session_jwt = resp.session_jwt;

                    return res.redirect("/dashboard");
                  });
                  ```
                </CodeGroup>
              </Tab>
            </Tabs>
          </Step>

          <Step title="Test it out">
            Run your application and go through an authentication flow with a user who already has an MFA method enabled.
          </Step>
        </Steps>
      </Tab>

      <Tab title="Optional enrollment">
        ### Optional MFA enrollment

        If an Organization's MFA policy is `OPTIONAL`, members can still opt to secure their account with MFA. At a high level, the flow for this is very similar to required enrollment, but can occur anytime after the Member has logged in and has been granted a Session.

        Add a route for optional enrollment that expects a `session_jwt` or `session_token` before calling the MFA Authentication endpoint that finalizes the enrollment. This can be used to add optional MFA to users or additional MFA methods to users who want to use both TOTP and SMS.

        <Tabs>
          <Tab title="SMS OTP">
            <CodeGroup>
              ```python Python icon=python theme={null}
              @app.route("/optional-mfa-enrollment", methods=["POST"])
              def optional_mfa_enrollment():
                  jwt = session.get("stytch_session_jwt")
                  if not jwt:
                      return "No session", 400

                  resp = stytch_client.sessions.authenticate_jwt(session_jwt=jwt)
                  if resp.status_code != 200:
                      return "Invalid session", 401

                  organization_id = resp.member_session.organization_id
                  member_id = resp.member_session.member_id
                  code = request.form.get("code")

                  resp = stytch_client.otps.sms.authenticate(
                      session_jwt=jwt,
                      code=code,
                      organization_id=organization_id,
                      member_id=member_id,
                      set_mfa_enrollment="enroll",
                  )

                  if resp.status_code != 200:
                      return "Error authenticating MFA", 500

                  session["stytch_session_jwt"] = resp.session_jwt
                  return redirect(url_for("dashboard"))
              ```

              ```javascript Node.js icon=node theme={null}
              app.post("/optional-mfa-enrollment", async (req, res) => {
                const jwt = req.session.stytch_session_jwt;
                if (!jwt) {
                  return res.status(400).send("No session");
                }

                const authResp = await stytchClient.sessions.authenticateJwt({
                  session_jwt: jwt,
                });

                const organizationId = authResp.member_session.organization_id;
                const memberId = authResp.member_session.member_id;
                const code = req.body.code;

                const resp = await stytchClient.otps.sms.authenticate({
                  session_jwt: jwt,
                  code: code,
                  organization_id: organizationId,
                  member_id: memberId,
                  set_mfa_enrollment: "enroll",
                });

                req.session.stytch_session_jwt = resp.session_jwt;
                return res.redirect("/dashboard");
              });
              ```
            </CodeGroup>
          </Tab>

          <Tab title="TOTP">
            <CodeGroup>
              ```python Python icon=python theme={null}
              @app.route("/optional-mfa-enrollment", methods=["POST"])
              def optional_mfa_enrollment():
                  jwt = session.get("stytch_session_jwt")
                  if not jwt:
                      return "No session", 400

                  resp = stytch_client.sessions.authenticate_jwt(session_jwt=jwt)
                  if resp.status_code != 200:
                      return "Invalid session", 401

                  organization_id = resp.member_session.organization_id
                  member_id = resp.member_session.member_id
                  code = request.form.get("code")

                  resp = stytch_client.totps.authenticate(
                      session_jwt=jwt,
                      code=code,
                      organization_id=organization_id,
                      member_id=member_id,
                      set_mfa_enrollment="enroll",
                  )

                  if resp.status_code != 200:
                      return "Error authenticating MFA", 500

                  session["stytch_session_jwt"] = resp.session_jwt
                  return redirect(url_for("dashboard"))
              ```

              ```javascript Node.js icon=node theme={null}
              app.post("/optional-mfa-enrollment", async (req, res) => {
                const jwt = req.session.stytch_session_jwt;
                if (!jwt) {
                  return res.status(400).send("No session");
                }

                const authResp = await stytchClient.sessions.authenticateJwt({
                  session_jwt: jwt,
                });

                const organizationId = authResp.member_session.organization_id;
                const memberId = authResp.member_session.member_id;
                const code = req.body.code;

                const resp = await stytchClient.totps.authenticate({
                  session_jwt: jwt,
                  code: code,
                  organization_id: organizationId,
                  member_id: memberId,
                  set_mfa_enrollment: "enroll",
                });

                req.session.stytch_session_jwt = resp.session_jwt;
                return res.redirect("/dashboard");
              });
              ```
            </CodeGroup>
          </Tab>
        </Tabs>

        If the Member is enrolling in an additional form of MFA (e.g. has SMS MFA already, and is adding TOTP) you can also specify that the newly enrolled MFA method becomes the default through the `set_default_mfa` boolean.

        If you want to enroll the user in MFA for occasional step-up authentication on particularly sensitive actions, rather than MFA that is required on each login you can omit the `set_mfa_enrollment` field in the `authenticate()` call.
      </Tab>
    </Tabs>
  </Tab>
</Tabs>
