/
Contact usSee pricingStart building
Node
​

    About Stytch

    Introduction
    Integration Approaches
      Full-stack overview
      Frontend (pre-built UI)
      Frontend (headless)
      Backend
    Migrations
      Migration overview
      Migrating users statically
      Migrating users dynamically
      Additional migration considerations
      Zero-downtime deployment
      Defining external IDs for users
      Exporting from Stytch
    Custom Domains
      Overview

    Authentication

    DFP Protected Auth
      Overview
      Setting up DFP Protected Auth
      Handling challenges
    Magic Links
    • Email Magic Links

      • Getting started with the API
        Getting started with the SDK
        Replacing your password reset flow
        Building an invite user flow
        Add magic links to an existing auth flow
        Adding PKCE to a Magic Link flow
        Magic Link redirect routing
    • Embeddable Magic Links

      • Getting started with the API
    MFA
      Overview
      Backend integration
      Frontend integration
    Mobile Biometrics
      Overview
    M2M Authentication
      Authenticate an M2M Client
      Rotate client secrets
      Import M2M Clients from Auth0
    OAuth
    • Identity providers

      • Overview
        Provider setup
      Getting started with the API (Google)
      Add Google One Tap via the SDK
      Email address behavior
      Adding PKCE to an OAuth flow
    Connected AppsBeta
      Setting up Connected Apps
      About Remote MCP Servers
    • Resources

      • Integrate with AI agents
        Integrate with MCP servers
        Integrate with CLI Apps
    Passcodes
      Getting started with the API
      Getting started with the SDK
    • Toll fraud

      • What is SMS toll fraud?
        How you can prevent toll fraud
      Unsupported countries
    Passkeys & WebAuthn
    • Passkeys

      • Passkeys overview
        Set up Passkeys with the frontend SDK
    • WebAuthn

      • Getting started with the API
        Getting started with the SDK
    Passwords
      Getting started with the API
      Getting started with the SDK
      Password strength policy
    • Email verification

      • Overview
        Email verification before password creation
        Email verification after password creation
    Sessions
      How to use sessions
      Backend integrations
      Frontend integrations
      Custom claims
      Custom claim templates
      Session tokens vs JWTs
      How to use Stytch JWTs
    TOTP
      Getting started with the API
      Getting started with the SDK
    Web3
      Getting started with the API
      Getting started with the SDK

    Authorization

    Implement RBAC with metadata

    3rd Party Integrations

    Planetscale
    Supabase
    Feathery
    Unit

    Testing

    E2E testing
    Sandbox values
Get support on SlackVisit our developer forum

Contact us

Consumer Authentication

/

Guides

/

Authentication

/

MFA

/

Frontend integration

Frontend MFA integration guide

In this guide, you'll learn how to create an MFA flow and enforce MFA in your application using Stytch's frontend JavaScript SDK. If you'd like to implement MFA, you'll need to use the frontend SDK's headless methods with your own UI, rather than the pre-built UI components.

This guide is only relevant to Consumer Stytch projects. If you have a B2B project, please check out our B2B MFA guides instead.

Prompt users to authenticate their primary factor

First, you'll need to prompt users to complete their first method of authentication. For the purposes of this guide, we'll use Passwords as our primary authentication factor.

We'll also assume that the user already has an account with a password and a verified email address. If the user's email address has not yet been verified, you will receive a too_many_unverified_factors error upon adding a new phone number.

Create a UI to prompt the user for their email address and password. Upon submission, call the Authenticate password method:

import { StytchHeadlessClient } from '@stytch/vanilla-js/headless';

const stytchClient = new StytchHeadlessClient('PUBLIC_TOKEN');

// In minutes, accepted range of 5 to 527040 minutes (366 days)
const DESIRED_SESSION_LENGTH = 120;

export const authenticatePassword = () => {
  stytchClient.passwords.authenticate({
    email: 'sandbox@stytch.com',
    password: 'PASSWORD',
    session_duration_minutes: DESIRED_SESSION_LENGTH
  })
  .then(resp => {
    // Stytch Session object
    const session = resp.session;
  })
  .catch(err => { console.log(err) });
};

In the Authenticate password response, you'll receive a session object with details about the user's new Stytch Session. Within the session object, you'll see an authentication_factors array with a password factor in it. Note that there is only one factor present in the authentication_factors array, indicating that the user has not yet completed MFA.

"session": {
    ...
    "authentication_factors": [
        {
            "created_at": "2024-10-30T16:31:13Z",
            "delivery_method": "knowledge",
            "last_authenticated_at": "2024-10-30T16:31:13Z",
            "type": "password",
            "updated_at": "2024-10-30T16:31:13Z"
        }
    ],
    ...
}

2
Prompt users to authenticate their secondary factor

In this guide, we'll use SMS OTP as our secondary factor.

Passwords and SMS OTP are a good combination of factors, because they confirm your user's identity in two different ways. Note that Passwords and an email-based authentication method are not a secure combination of factors, given that a user's password can be reset if a bad actor has access to the user's email inbox.

Create a UI for the user to enter their phone number if it's not already present on their Stytch User, and send the user an OTP code via the Send OTP by SMS method:

export const sendPasscode = () => {
  stytchClient.otps.sms.send('+15555555555')
  .then(resp => {
    // Save the method_id for use in the Authenticate OTP call
    const method_id = resp.method_id;
  })
  .catch(err => { console.log(err) });
};

Next, surface a UI for the user to input the OTP code that they receive, and call the Authenticate OTP endpoint with the method_id from the stytchClient.otps.sms.send response and the OTP code:

export const authenticate = () => {
  stytchClient.otps.authenticate(
    'OTP_CODE',
    'METHOD_ID_FROM_SEND_OTP_RESPONSE'
  )
  .then(resp => {
    // Stytch Session object
    const session = resp.session;
  })
  .catch(err => { console.log(err) });
};

The new session object's authentication_factors array will now contain both a password factor and a new otp factor:

"session": {
    ...
    "authentication_factors": [
        {
            "created_at": "2024-10-30T16:31:13Z",
            "delivery_method": "knowledge",
            "last_authenticated_at": "2024-10-30T16:31:13Z",
            "type": "password",
            "updated_at": "2024-10-30T16:31:13Z"
        },
        {
            "created_at": "2024-10-30T16:32:11Z",
            "delivery_method": "sms",
            "last_authenticated_at": "2024-10-30T16:32:11Z",
            "phone_number_factor": {
                "phone_id": "phone-number-...",
                "phone_number": "+15555555555"
            },
            "type": "otp",
            "updated_at": "2024-10-30T16:32:11Z"
        }
    ],
    ...
},

If you authenticate either factor on the backend, make sure to hydrate the frontend SDK with the updated session data by passing the newest session_token into the frontend Update session method, followed by a call to the frontend Authenticate session method. For security purposes, the original session will be revoked shortly after the user authenticates their second factor.

3
Enforce MFA in your application's authorization logic

In order to enforce that users complete MFA before accessing protected content, you'll need to inspect the authentication_factors array after authenticating the Stytch Session in your application's backend authorization logic and make sure that the required factors are present. Below is a simplified example of a backend authorization endpoint:

const stytch = require('stytch');

const stytchClient = new stytch.Client({
  project_id: 'PROJECT_ID',
  secret: 'SECRET',
});

export const authorizeRequest = (session_token) => {
  stytchClient.sessions.authenticate({session_token})
  .then(resp => { 
    const authentication_factors = resp.session.authentication_factors;

    if (
      session.authentication_factors.length === 2 &&
      session.authentication_factors.find((i) => i.delivery_method === 'knowledge') &&
      session.authentication_factors.find((i) => i.delivery_method === 'sms')
    ) {
      // User has completed MFA; can proceed with request
    } else {
      // User has not completed MFA; send to login flow
    }
   })
  .catch(err => {
    // Session could not be successfully authenticated; send to login flow
  });
}

You can also gate frontend content based on the Session's authentication factors in order to achieve your desired UI. Below is a simplified example:

Vanilla JS
​

4
(Optional) Add step-up authentication

You may also choose to implement step-up authentication, where you prompt users to authenticate a second factor before allowing them to take particularly sensitive actions.

When using step-up authentication, we generally recommend inspecting the last_authenticated_at value on each authentication factor and prompting the user to reauthenticate their secondary factor if too much time has passed since they last did so:

export const authorizeSensitiveRequest = (session_token) => {
  stytchClient.sessions.authenticate({session_token})
    .then(resp => { 
      const authentication_factors = resp.session.authentication_factors;

      if (
        authentication_factors.length === 2 &&
        authentication_factors.find((i) => i.delivery_method === 'knowledge') &&
        authentication_factors.find((i) => (
          i.delivery_method === 'sms' &&
          // Check if SMS OTP was completed within the last 5 minutes
          Date.now() - new Date(i.last_authenticated_at).getTime() < 5 * 60 * 1000
        ))
      ) {
        // User has completed SMS OTP flow within the past 5 minutes; can proceed with request
      } else {
        // User has not completed SMS OTP flow within the past 5 minutes; deny request and trigger 2FA flow
      }
     })
    .catch(err => {
      // Session could not be successfully authenticated; send to login flow
    });
}

What's next?

We recommend checking out this resource for some important frontend SDK MFA security notes.

Prompt users to authenticate their primary factor

2.

Prompt users to authenticate their secondary factor

3.

Enforce MFA in your application's authorization logic

4.

(Optional) Add step-up authentication

What's next?