Skip to content
Esc
navigateopen⌘Jpreview
Dashboard
On this page

Embed the pre-built UI component

Embed and customize a prebuilt MFA UI component.

Overview

Before you start

These instructions only apply to interfaces that use the pre-built UI components. If you are using a custom UI, the embed instructions depend on your implementation details.

The tutorial configures TOTP as a secondary factor, but the same set of steps are applicable for other secondary factor types.


Render the TOTP Widget in a page

The following example shows the scenario where you have a dedicated route, such as /totp, for rendering the TOTP Widget. Upon a successful login, the user will be automatically redirected to the return value of getRedirectionURL (defaulting to /).

With React Router

React Router v6
import SuperTokens from "supertokens-auth-react";
import TOTP from "supertokens-auth-react/recipe/totp";
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui";
import Header from "./header";
import Footer from "./footer";
import { useNavigate } from "react-router-dom";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    TOTP.init({
      totpMFAScreen: {
        disableDefaultUI: true,
      },
    }),
    MultiFactorAuth.init({
      getRedirectionURL: async (context) => {
        if (context.action === "GO_TO_FACTOR") {
          if (context.factorId === "totp") {
            return "/totp";
          }
        }
      },
    }),
    // ...
  ],
});

function TOTPPage() {
  const navigate = useNavigate();
  return (
    <div>
      <Header />
      <MFATOTP navigate={navigate} />
      <Footer />
    </div>
  );
}

In the above code snippet, we:

  1. Disabled the default TOTP UI by setting disableDefaultUI to true inside the TOTP recipe config.
  2. Overrode the getRedirectionURL function inside the MFA recipe config to redirect to /totp whenever we want to show the TOTP factor.

Feel free to customize the redirection URLs as needed.


Render the TOTP Widget in a popup

The following example shows the scenario where you embed the TOTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth.

With React Router

React Router v6
import React, { useEffect, useRef, useState } from "react";
import Modal from "react-modal";
import SuperTokens from "supertokens-auth-react";
import TOTP from "supertokens-auth-react/recipe/totp";
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui";
import Session from "supertokens-auth-react/recipe/session";
import { useLocation, useNavigate } from "react-router-dom";

SuperTokens.init({
  appInfo: {
    apiDomain: "...",
    appName: "...",
    websiteDomain: "...",
  },
  recipeList: [
    TOTP.init(/* ... */),
    MultiFactorAuth.init(/* ... */),
    // ...
  ],
});

function TOTPPopup() {
  const sessionContext = Session.useSessionContext();
  const navigate = useNavigate();
  const location = useLocation();
  const retryStarted = useRef(false);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [error, setError] = useState<string>();

  const openModal = () => {
    const url = new URL(window.location.href);
    const returnUrl = new URL(url);
    returnUrl.searchParams.delete("stepUp");
    returnUrl.searchParams.delete("redirectToPath");
    returnUrl.searchParams.set("retryProtectedOperation", "true");

    url.searchParams.delete("retryProtectedOperation");
    url.searchParams.set("stepUp", "true");
    url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`);
    window.history.replaceState(window.history.state, "", url);
    retryStarted.current = false;
    setError(undefined);
    setIsModalOpen(true);
  };

  const cancelModal = () => {
    const url = new URL(window.location.href);
    url.searchParams.delete("stepUp");
    url.searchParams.delete("redirectToPath");
    url.searchParams.delete("retryProtectedOperation");
    window.history.replaceState(window.history.state, "", url);
    setIsModalOpen(false);
  };

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) {
      return;
    }

    retryStarted.current = true;
    void (async () => {
      try {
        await MultiFactorAuth.resyncSessionAndFetchMFAInfo();
        const response = await fetch("/api/sensitive-operation", { method: "POST" });
        if (!response.ok) {
          throw new Error("The protected operation was rejected");
        }

        const url = new URL(window.location.href);
        url.searchParams.delete("retryProtectedOperation");
        navigate(`${url.pathname}${url.search}${url.hash}`, { replace: true });
        setIsModalOpen(false);
      } catch (error) {
        setError(error instanceof Error ? error.message : "The protected operation failed");
      }
    })();
  }, [location.search, navigate]);

  if (sessionContext.loading) {
    return null;
  }

  return (
    <div style={{ textAlign: "center" }}>
      {
        <Session.SessionAuth>
          <h2>You are logged In! </h2>
          <h3>UserId: {sessionContext.userId}</h3>
          <button onClick={openModal}>Verify with TOTP</button>
          <button onClick={() => Session.signOut()}>Sign Out</button>
        </Session.SessionAuth>
      }

      {error !== undefined && <p role="alert">{error}</p>}

      <Modal isOpen={isModalOpen} onRequestClose={cancelModal} contentLabel="TOTP challenge">
        <button onClick={cancelModal}>Cancel</button>
        <MFATOTP navigate={navigate} />
      </Modal>
    </div>
  );
}

The retryProtectedOperation return marker is not proof that step-up authentication succeeded. After the factor flow returns, call MultiFactorAuth.resyncSessionAndFetchMFAInfo() to synchronize the session and retry the server-protected operation. The MFA freshness validator on the backend is authoritative and must reject the operation if the required factor is missing or too old. The Cancel button only cancels the popup; it must not retry or authorize the operation.

API reference

API schema and response details