Headless Integration of MFA

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

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

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

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.

2Start 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 { useStytch } from '@stytch/react/b2b';

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

  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>

3Authenticate 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 { useStytch } from '@stytch/b2b/react';

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>

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