/
Contact usSee pricingStart building

    About B2B Saas Authentication

    Introduction
    Stytch B2B Basics
    Integration Approaches
      Full-stack overview
      Frontend (pre-built UI)
      Frontend (headless)
      Backend
    Next.js
      Routing
      Authentication
      Sessions
    Migrations
      Overview
      Reconciling data models
      Migrating user data
      Additional migration considerations
      Zero-downtime deployment
      Defining external IDs for members
      Exporting from Stytch
    Custom Domains
      Overview

    Authentication

    Single Sign On
    • Resources

      • Overview
        External SSO Connections
        Standalone SSO
    • Integration Guides

      • Start here
        Backend integration guide
        Headless integration guide
        Pre-built UI integration guide
    OAuth
    • Resources

      • Overview
        Authentication flows
        Identity providers
        Google One Tap
        Provider setup
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Connected AppsBeta
      Setting up Connected Apps
      About Remote MCP Servers
    • Resources

      • Integrate with AI agents
        Integrate with a remote MCP server
    Sessions
    • Resources

      • Overview
        JWTs vs Session Tokens
        How to use Stytch JWTs
        Custom Claims
    • Integration Guides

      • Start here
        Backend integration
        Frontend integration
    Email OTP
      Overview
    Magic Links
    • Resources

      • Overview
        Email Security Scanner Protections
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Multi-Factor Authentication
    • Resources

      • Overview
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
        Pre-built UI frontend integration
    Passwords
      Overview
      Strength policies
    UI components
      Overview
      Implement the Discovery flow
      Implement the Organization flow
    DFP Protected Auth
      Overview
      Setting up DFP Protected Auth
      Handling challenges
    M2M Authentication
      Authenticate an M2M Client
      Rotate client secrets
      Import M2M Clients from Auth0

    Authorization & Provisioning

    RBAC
    • Resources

      • Overview
        Stytch Resources & Roles
        Role assignment
    • Integration Guides

      • Start here
        Backend integration
        Headless frontend integration
    SCIM
    • Resources

      • Overview
        Supported actions
    • Integration Guides

      • Using Okta
        Using Microsoft Entra
    Organizations
      Managing org settings
      JIT Provisioning

    Testing

    E2E testing
    Sandbox values
Get support on SlackVisit our developer forum

Contact us

B2B Saas Authentication

/

Guides

/

Authentication

/

Multi-Factor Authentication

/

Integration Guides

/

Headless frontend integration

Headless Integration of MFA

This guide covers enrollment and authentication of MFA using Stytch's headless frontend SDKs, and your own UI.

Required MFA Enrollment

The below sequence outlines the expected flow after primary authentication, when an Organization's MFA Policy is REQUIRED_FOR_ALL but the Member is not currently enrolled in MFA.

Headless integration of required MFA enrollment

1
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()
  • oauth.authenticate()
  • magicLinks.authenticate()
  • passwords.authenticate()
  • discovery.intermediateSessions.exchange()
  • passwords.resetByEmail()
  • passwords.resetByExistingPassword()

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

{
	"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.

2
Start MFA enrollment

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”.

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: '+14155551234'
    });

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.

Using our React SDK this will look like the following:

import React, { 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>;
};

In vanilla Javascript you would do:

<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>

3
Authenticate to complete enrollment

Prompt the user to input their OTP or TOTP code in order to complete the MFA enrollment and authenticate.

If the user has decided to enroll in authenticator app TOTP, you would do the following in React:

import React, { useCallback, useState } from 'react';
import { useStytchB2BClient } from '@stytch/react/b2b';

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

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

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

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

In vanilla Javascript this would look like:

<button onclick="authenticate()">Authenticate TOTP</button>
<script>
    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

    const authenticate = () => {
    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>

If the user has decided to enroll in SMS OTP, you would do the following in React:

import React, { 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,
      });
    },
    [stytch, code],
  );

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

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

In vanilla Javascript this would look like:

<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>

4
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.

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.

Headless integration of returning MFA authentication

1
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:

{
	"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.

2
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.

If the user's default MFA method is TOTP', you would do the following in React:

import React, { useCallback, useState } from 'react';
import { useStytchB2BClient } from '@stytch/react/b2b';

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

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

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

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

In vanilla Javascript this would look like:

<button onclick="authenticate()">Authenticate TOTP</button>
<script>
    import { StytchB2BHeadlessClient } from '@stytch/vanilla-js/b2b/headless';
    const stytch = new StytchB2BHeadlessClient('STYTCH_PUBLIC_TOKEN');

    const authenticate = () => {
    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>

If the user's default MFA method is SMS OTP, you would do the following in React:

import React, { 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,
      });
    },
    [stytch, code],
  );

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

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

In vanilla Javascript this would look like:

<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>

3
Test it out

Run your application and go through an authentication flow with a user who already has an MFA method enabled.

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.

Headless integration of optional MFA enrollment

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:

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('totp-input').value,
      session_duration_minutes: 60,
      set_mfa_enrollment: `enroll`
    });


  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`
  });

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.

Required MFA Enrollment

1.

Detect when MFA enrollment required

2.

Start MFA enrollment

3.

Authenticate to complete enrollment

4.

Test it out

Returning MFA Authentication

1.

Detect user's MFA options

2.

Authenticate MFA

3.

Test it out

Optional MFA Enrollment