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 /).
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>
);
}import React from "react";
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 { useHistory } from "react-router-dom5";
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 history = useHistory();
return (
<div>
<Header />
<MFATOTP navigate={history} />
<Footer />
</div>
);
}import React from "react";
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";
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() {
return (
<div>
<Header />
<MFATOTP />
<Footer />
</div>
);
}In the above code snippet, we:
- Disabled the default TOTP UI by setting
disableDefaultUItotrueinside the TOTP recipe config. - Overrode the
getRedirectionURLfunction inside the MFA recipe config to redirect to/totpwhenever 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.
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>
);
}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 { useHistory, useLocation } from "react-router-dom5";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
TOTP.init(/* ... */),
MultiFactorAuth.init(/* ... */),
// ...
],
});
function TOTPPopup() {
const sessionContext = Session.useSessionContext();
const history = useHistory();
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");
history.replace(`${url.pathname}${url.search}${url.hash}`);
setIsModalOpen(false);
} catch (error) {
setError(error instanceof Error ? error.message : "The protected operation failed");
}
})();
}, [history, location.search]);
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={history} />
</Modal>
</div>
);
}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";
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
recipeList: [
TOTP.init(/* ... */),
MultiFactorAuth.init(/* ... */),
// ...
],
});
function TOTPPopup() {
const sessionContext = Session.useSessionContext();
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(window.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");
window.history.replaceState(window.history.state, "", url);
setIsModalOpen(false);
} catch (error) {
setError(error instanceof Error ? error.message : "The protected operation failed");
}
})();
}, []);
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 />
</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.