Docs

MFA with Expo

Multi-factor authentication provides your users with a second verification check during sign-in, drastically improving your user's security. The examples in this guide demonstrate how to build a custom multi-factor authentication (MFA) flow in Expo with Clerk.

Before you start

  1. Follow Clerk's Expo quickstart to ensure your project is configured properly.
  2. Because this example uses expo-router's experimental API routes, you must follow the Expo docs to deploy your routes to a domain. Doing so allows Clerk to access to your CLERK_SECRET_KEY, which you must pass to your impersonation endpoint.
  3. You must configure your Clerk instance to enable email and password authentication:
    1. Navigate to the Clerk Dashboard and in the navigation sidebar, go to User & Authentication > Email, Phone, and Username.
    2. Ensure that Email address is required, and Username is not required.
    3. Next to Email address, select the settings cog icon to ensure that the email address verification method is set to Email code.
    4. In the Authentication strategies section of this page, ensure Password is enabled.
  4. You must also configure your Clerk instance to enable multi-factor authentication:
    1. Navigate to your Clerk Dashboard and in the navigation sidebar, select User & Authentication > Multi-factor.
    2. For this example, enable the Authenticator application and Backup codes strategies.
  5. Install the expo-checkbox package for the UI & Clerk types lib:
    terminal
    npm install expo-checkbox @clerk/types
    terminal
    yarn add expo-checkbox @clerk/types
    terminal
    pnpm add expo-checkbox @clerk/types

Create an MFA component

The following example demonstrates a full MFA sign-in flow that enables users to generate QR codes which can be used with an authenticator app.

Note

You can render this component in a custom sign-in or sign-up flow, which you can find examples of in the Expo quickstart.

Create the sign-in form

In your components directory, create the MFA sign-in form.

components/SignInMFAForm.tsx
import React from "react";
import { useSignIn } from "@clerk/clerk-expo";
import { useRouter } from "expo-router";
import { Text, TextInput, Button, View } from "react-native";
import Checkbox from "expo-checkbox";

export default function SignInMFAForm() {
  const { signIn, setActive, isLoaded } = useSignIn();

  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [code, setCode] = React.useState("");
  const [useBackupCode, setUseBackupCode] = React.useState(false);
  const [displayTOTP, setDisplayTOTP] = React.useState(false);
  const router = useRouter();
  
  const handleFirstStage = async () => {
    if (!isLoaded) return;

    try {
      const attemptFirstFactor = await signIn.create({
        identifier: email,
        password,
      });

      if (attemptFirstFactor.status === "complete") {
        await setActive({ session: attemptFirstFactor.createdSessionId });
        router.replace("/dashboard");
      } else if (attemptFirstFactor.status === "needs_second_factor") {
        setDisplayTOTP(true);
      } else {
        console.error(JSON.stringify(attemptFirstFactor, null, 2));
      }
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2));
    }
  };

  const onPressTOTP = React.useCallback(async () => {
    if (!isLoaded) return;
    
    try {      
      // Attempt the TOTP or backup code verification
      const attemptSecondFactor = await signIn.attemptSecondFactor({
        strategy: useBackupCode ? "backup_code" : "totp",
        code: code,
      });

      // If verification was completed, set the session to active
      // and redirect the user
      if (attemptSecondFactor.status === "complete") {
        await setActive({ session: attemptSecondFactor.createdSessionId });

        router.replace("/dashboard");
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(attemptSecondFactor,null,2));
      }
    } catch (err: any) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2));
    }
  }, [isLoaded, email, password, code, useBackupCode]);

  if (displayTOTP) {
    return (
      <View>
        <Text>Verify your account</Text>

        <View>
          <Text>Code</Text>
          <TextInput
            value={code}
            placeholder="OTP or Backup Code"
            onChangeText={(c) => setCode(c)}
          />
        </View>
        <View>
          <Text>This code is a backup code</Text>
          <Checkbox
            value={useBackupCode}
            onValueChange={() => setUseBackupCode((prev) => !prev)}
          />
        </View>
        <Button title="Verify" onPress={onPressTOTP} />
      </View>
    );
  }

  return (
    <View>
      <Text>Sign In</Text>

      <TextInput
        value={email}
        placeholder="Email..."
        placeholderTextColor="#000"
        onChangeText={(email) => setEmail(email)}
      />

      <TextInput
        value={password}
        placeholder="Password..."
        placeholderTextColor="#000"
        onChangeText={(password) => setPassword(password)}
      />

      <Button title="Sign In" onPress={handleFirstStage} />
    </View>
  );
}

Create a (dashboard) route

For this example, to let users configure MFA, create a basic dashboard. Start by creating the (dashboard) route group and adding the following code to the layout:

app/(dashboard)/_layout.tsx
import { Redirect, Stack } from "expo-router";
import { useAuth } from "@clerk/clerk-expo";

export default function AuthenticatedLayout() {
  const { isSignedIn } = useAuth();

  if (!isSignedIn) {
    return <Redirect href={"/sign-in"} />;
  }

  return <Stack />;
}

Create an account page

Next, add a basic account page to the (dashboard) route group that shows users whether or not MFA is enabled, and allows them to add MFA with an authenticator app.

app/(dashboard)/account.tsx
import React from "react";
import { useUser } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import { View, Text, Button, TouchableOpacity, FlatList } from "react-native";
import { BackupCodeResource } from "@clerk/types";

export default function ManageTOTPMfa() {
  const [backupCodes, setBackupCodes] = React.useState<
    BackupCodeResource | undefined
  >(undefined);
  const [loading, setLoading] = React.useState(false);

  const { isLoaded, user } = useUser();

  if (!isLoaded || !user) return null;

  const generateBackupCodes = () => {
    setLoading(true);
    void user
      ?.createBackupCode()
      .then((backupCodes: BackupCodeResource) => {
        setBackupCodes(backupCodes);
        setLoading(false);
      })
      .catch((error) => {
        console.log("Error:", error);
        setLoading(false);
      });
  };

  const disableTOTP = async () => {
    await user.disableTOTP();
  };

  const MFAEnabled = () => {
    return (
      <View>
        <Text>TOTP via authentication app enabled - </Text>
        <Button onPress={() => disableTOTP()} title="Remove" />
      </View>
    );
  };

  const MFADisabled = () => {
    return (
      <View>
        <Text>Add TOTP via authentication app - </Text>
        <TouchableOpacity>
          <Link href="/add-mfa">
            <Text>Add</Text>
          </Link>
        </TouchableOpacity>
      </View>
    );
  };

  return (
    <>
      <Text>Current MFA Settings</Text>

      <Text>Authenticator App</Text>

      {user.totpEnabled ? <MFAEnabled /> : <MFADisabled />}

      {user.backupCodeEnabled && (
        <View>
          <Text>Backup Codes</Text>
          {loading && <Text>Loading...</Text>}
          {backupCodes && !loading && (
            <FlatList
              data={backupCodes.codes}
              renderItem={(code) => <Text>{code.item}</Text>}
              keyExtractor={(item) => item}
            />
          )}
          <Button
            onPress={() => generateBackupCodes()}
            title="Regenerate Codes"
          />
        </View>
      )}
    </>
  );
}
=

Create the QR code generator

Finally, create a page that adds the functionality for generating the QR code and back up codes.

app/(dashboard)/add-mfa.tsx
import React from "react";
import { useUser } from "@clerk/clerk-expo";
import { Link } from "expo-router";
import { QrCodeSvg } from "react-native-qr-svg";
import { FlatList, Button, Text, TextInput, View } from "react-native";

import { BackupCodeResource, TOTPResource } from "@clerk/types";

type AddTotpSteps = "add" | "verify" | "backupcodes" | "success";
type DisplayFormat = "qr" | "uri";

function AddTOTPMfa({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>;
}) {
  const [totp, setTotp] = React.useState<TOTPResource | undefined>(undefined);
  const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>("qr");
  const { user } = useUser();

  React.useEffect(() => {
    void user
      ?.createTOTP()
      .then((totp: TOTPResource) => setTotp(totp))
      .catch((error) => console.log("error", error));
  }, []);

  return (
    <View>
      <Text>Add TOTP MFA</Text>

      {totp && displayFormat === "qr" && (
        <>
          <View>
            <QrCodeSvg value={totp?.uri || ""} frameSize={200} />
          </View>
          <Button title="Use URI" onPress={() => setDisplayFormat("uri")} />
        </>
      )}

      {totp && displayFormat === "uri" && (
        <>
          <View>
            <Text>{totp.uri}</Text>
          </View>
          <Button title="Use QR Code" onPress={() => setDisplayFormat("qr")} />
        </>
      )}

      <Button title="Verify" onPress={() => setStep("verify")} />
      <Button title="Reset" onPress={() => setStep("add")} />
    </View>
  );
}

function VerifyMFA({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>;
}) {
  const [code, setCode] = React.useState("");

  const { user } = useUser();

  const verifyTotp = async (e: any) => {
    await user
      ?.verifyTOTP({ code })
      .then(() => setStep("backupcodes"))
      .catch((error) => console.log("Error", error));
  };

  return (
    <>
      <Text>Verify MFA</Text>
      <TextInput value={code} onChangeText={(c) => setCode(c)} />
      <Button onPress={verifyTotp} title="Verify Code" />
      <Button onPress={() => setStep("add")} title="Reset" />
    </>
  );
}

function BackupCodes({
  setStep,
}: {
  setStep: React.Dispatch<React.SetStateAction<AddTotpSteps>>;
}) {
  const { user } = useUser();
  const [backupCode, setBackupCode] = React.useState<
    BackupCodeResource | undefined
  >(undefined);

  React.useEffect(() => {
    if (backupCode) {
      return;
    }

    void user
      ?.createBackupCode()
      .then((backupCode: BackupCodeResource) => setBackupCode(backupCode))
      .catch((error) => console.log("Error", error));
  }, []);

  return (
    <>
      <Text>Save Backup Codes</Text>
      {backupCode && (
        <View>
          <Text>
            Save this list of backup codes somewhere safe in case you need to
            access your account in an emergency
          </Text>

          <FlatList
            data={backupCode.codes.map((code) => ({
              key: code,
            }))}
            renderItem={({ item }) => <Text>{item.key}</Text>}
          />

          <Button title="Finish" onPress={() => setStep("success")} />
        </View>
      )}
    </>
  );
}

function Success() {
  return (
    <>
      <Text>Success</Text>
      <Text>
        You successfully added TOTP Mfa via an authentication application
      </Text>
    </>
  );
}

export default function AddMfaScreen() {
  const [step, setStep] = React.useState<AddTotpSteps>("add");

  return (
    <>
      {step === "add" && <AddTOTPMfa setStep={setStep} />}
      {step === "verify" && <VerifyMFA setStep={setStep} />}
      {step === "backupcodes" && <BackupCodes setStep={setStep} />}
      {step === "success" && <Success />}

      <Link href="/account">
        <Text>Manage MFA</Text>
      </Link>
    </>
  );
}

Feedback

What did you think of this content?