Docs

Build a custom email/password authentication flow

Caution

This guide is for users who want to build a custom user interface using the Clerk API. To authenticate users with email and password using a prebuilt UI, you should use Clerk's Account Portal pages or prebuilt components.

This guide will walk you through how to build a custom email/password sign-up and sign-in flow.

Enable email and password authentication

To use email and password authentication, you first need to enable these authentication strategies in the Clerk Dashboard.

  1. Navigate to the Clerk Dashboard.
  2. Go to User & Authentication > Email, Phone, and Username in the sidebar menu.
  3. Ensure that only Email address is required. Use the settings cog icon to check if a setting is required or optional. By default, the email address verification method is set to Email code. This means that when a user signs up using their email address and password, Clerk will send a one-time code to the user's email address. The user must then enter this code to verify their email and complete the sign-up process.
  4. In the Username section, ensure that Username is not required, or else the create() method will require a username to be passed in the params. If you would like to use usernames, you must handle collecting the username in your custom flow.
  5. In the Authentication strategies section of this page, ensure Password is enabled.

Create the email/password sign-up flow

To sign up a user using their email and password, you must:

  1. Initiate the sign-up process by collecting the user's email address and password.
  2. Prepare the email address verification, which sends a one-time code to the given address.
  3. Attempt to complete the email address verification by supplying the one-time code.
  4. If the email address verification is successful, set the newly created session as the active session.

This example is written for Next.js App Router but it can be adapted for any React meta framework, such as Remix or Gatsby.

app/sign-up/[[...sign-up]]/page.tsx
'use client';

import * as React from 'react';
import { useSignUp } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

export default function Page() {
  const { isLoaded, signUp, setActive } = useSignUp();
  const [emailAddress, setEmailAddress] = React.useState('');
  const [password, setPassword] = React.useState('');
  const [verifying, setVerifying] = React.useState(false);
  const [code, setCode] = React.useState('');
  const router = useRouter();

  // Handle submission of the sign-up form
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!isLoaded) return;

    // Start the sign-up process using the email and password provided
    try {
      await signUp.create({
        emailAddress,
        password,
      });

      // Send the user an email with the verification code
      await signUp.prepareEmailAddressVerification({
        strategy: 'email_code',
      });

      // Set 'verifying' true to display second form
      // and capture the OTP code
      setVerifying(true);
    } 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));
    }
  };

  // Handle the submission of the verification form
  const handleVerify = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!isLoaded) return;

    try {
      // Use the code the user provided to attempt verification
      const completeSignUp = await signUp.attemptEmailAddressVerification({
        code,
      });

      // If verification was completed, set the session to active
      // and redirect the user
      if (completeSignUp.status === 'complete') {
        await setActive({ session: completeSignUp.createdSessionId });
        router.push('/');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(completeSignUp, null, 2));
      }
    } catch (err: any) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2));
    }
  };

  // Display the verification form to capture the OTP code
  if (verifying) {
    return (
      <>
        <h1>Verify your email</h1>
        <form onSubmit={handleVerify}>
          <label id="code">Enter your verification code</label>
          <input
            value={code}
            id="code"
            name="code"
            onChange={(e) => setCode(e.target.value)}
          />
          <button type="submit">Verify</button>
        </form>
      </>
    );
  }

  // Display the initial sign-up form to capture the email and password
  return (
    <>
      <h1>Sign up</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input
            id="email"
            type="email"
            name="email"
            value={emailAddress}
            onChange={(e) => setEmailAddress(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input
            id="password"
            type="password"
            name="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <div>
          <button type="submit">Next</button>
        </div>
      </form>
    </>
  );
}

For the following example, your HTML file should look like this:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-up">
      <h2>Sign up</h2>
      <form id="sign-up-form">
        <label for="email">Enter email address</label>
        <input type="email" name="email" id="sign-up-email" />
        <label for="password">Enter password</label>
        <input type="password" name="password" id="sign-up-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your email</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      type="module"
      src="/src/main.js"
      async
      crossorigin="anonymous"
    ></script>
  </body>
</html>

And your JavaScript file should look like this:

main.js
import { Clerk } from '@clerk/clerk-js';

// Initialize Clerk with your Clerk publishable key
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY');
await clerk.load();

if (clerk.user) {
  // Mount user button component
  document.getElementById('signed-in').innerHTML = `
    <div id="user-button"></div>
  `;

  const userbuttonDiv = document.getElementById('user-button');

  clerk.mountUserButton(userbuttonDiv);
} else {
  // Handle the sign-up form
  document
    .getElementById('sign-up-form')
    .addEventListener('submit', async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const emailAddress = formData.get('email');
      const password = formData.get('password');

      try {
        // Start the sign-up process using the phone number method
        await clerk.client.signUp.create({ emailAddress, password });
        await clerk.client.signUp.prepareEmailAddressVerification();
        // Hide sign-up form
        document.getElementById('sign-up').setAttribute('hidden', '');
        // Show verification form
        document.getElementById('verifying').removeAttribute('hidden');
      } catch (error) {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(error);
      }
    });

  // Handle the verification form
  document.getElementById('verifying').addEventListener('submit', async (e) => {
    const formData = new FormData(e.target);
    const code = formData.get('code');

    try {
      // Verify the phone number
      const verify = await clerk.client.signUp.attemptEmailAddressVerification({
        code,
      });

      // Now that the user is created, set the session to active.
      await clerk.setActive({ session: verify.createdSessionId });
    } catch (error) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(error);
    }
  });
}
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-up">
      <h2>Sign up</h2>
      <form id="sign-up-form">
        <label for="email">Enter email address</label>
        <input type="email" name="email" id="sign-up-email" />
        <label for="password">Enter password</label>
        <input type="password" name="password" id="sign-up-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <form id="verifying" hidden>
      <h2>Verify your email</h2>
      <label for="code">Enter your verification code</label>
      <input id="code" name="code" />
      <button type="submit" id="verify-button">Verify</button>
    </form>

    <script
      async
      crossorigin="anonymous"
      data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
      src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
      type="text/javascript"
    ></script>

    <script>
      window.addEventListener('load', async function () {
        await Clerk.load();

        if (Clerk.user) {
          // Mount user button component
          document.getElementById('signed-in').innerHTML = `
            <div id="user-button"></div>
          `;

          const userbuttonDiv = document.getElementById('user-button');

          Clerk.mountUserButton(userbuttonDiv);
        } else {
          // Handle the sign-up form
          document
            .getElementById('sign-up-form')
            .addEventListener('submit', async (e) => {
              e.preventDefault();

              const formData = new FormData(e.target);
              const emailAddress = formData.get('email');
              const password = formData.get('password');

              try {
                // Start the sign-up process using the phone number method
                await Clerk.client.signUp.create({ emailAddress, password });
                await Clerk.client.signUp.prepareEmailAddressVerification();
                // Hide sign-up form
                document.getElementById('sign-up').setAttribute('hidden', '');
                // Show verification form
                document.getElementById('verifying').removeAttribute('hidden');
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });

          // Handle the verification form
          document
            .getElementById('verifying')
            .addEventListener('submit', async (e) => {
              const formData = new FormData(e.target);
              const code = formData.get('code');

              try {
                // Verify the phone number
                const verify =
                  await Clerk.client.signUp.attemptEmailAddressVerification({
                    code,
                  });

                // Now that the user is created, set the session to active.
                await Clerk.setActive({ session: verify.createdSessionId });
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });
        }
      });
    </script>
  </body>
</html>

First create a route group with a layout that redirects users if they're already signed in, as shown in the following example:

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

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

  if (isSignedIn) {
    return <Redirect href={"/dashboard"} />;
  }

  return <Stack />;
}

Then create a sign-up page that sends users an email verification code to create an account.

/app/(auth)/sign-up.tsx

import * as React from "react";
import { Text, TextInput, TouchableOpacity, View } from "react-native";
import { useSignUp } from "@clerk/clerk-expo";

export default function SignUpScreen() {
  const { isLoaded, signUp, setActive } = useSignUp();

  const [firstName, setFirstName] = React.useState("");
  const [lastName, setLastName] = React.useState("");
  const [emailAddress, setEmailAddress] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [pendingVerification, setPendingVerification] = React.useState(false);
  const [code, setCode] = React.useState("");

  const onSignUpPress = async () => {
    if (!isLoaded) {
      return;
    }

    // Start the sign-up process using the info the user provided
    try {
      await signUp.create({
        firstName,
        lastName,
        emailAddress,
        password,
      });

      // Send the user an email with the verification code
      await signUp.prepareEmailAddressVerification({ strategy: "email_code" });

      // Display the second form to collect the verification code
      setPendingVerification(true);
    } 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));
    }
  };

  // Handle the submission of the verification form
  const onPressVerify = async () => {
    if (!isLoaded) {
      return;
    }

    // Use the code the user provided to attempt verification
    try {
      const signUpAttempt = await signUp.attemptEmailAddressVerification({
        code,
      });

      // If verification was completed, set the session to active
      // and redirect the user
      if (signUpAttempt.status === 'complete') {
        await setActive({ session: signUpAttempt.createdSessionId });
        router.replace('/dashboard');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signUpAttempt, null, 2));
      }
    } catch (err: any) {
      console.error(JSON.stringify(err, null, 2));
    }
  };

  return (
    <View>
      {!pendingVerification && (
        <View>
          <View>
            <TextInput
              autoCapitalize="none"
              value={firstName}
              placeholder="First Name..."
              onChangeText={(firstName) => setFirstName(firstName)}
            />
          </View>
          <View>
            <TextInput
              autoCapitalize="none"
              value={lastName}
              placeholder="Last Name..."
              onChangeText={(lastName) => setLastName(lastName)}
            />
          </View>
          <View>
            <TextInput
              autoCapitalize="none"
              value={emailAddress}
              placeholder="Email..."
              onChangeText={(email) => setEmailAddress(email)}
            />
          </View>

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

          <TouchableOpacity onPress={onSignUpPress}>
            <Text>Sign up</Text>
          </TouchableOpacity>
        </View>
      )}
      {pendingVerification && (
        <View>
          <View>
            <TextInput
              value={code}
              placeholder="Code..."
              onChangeText={(code) => setCode(code)}
            />
          </View>
          <TouchableOpacity onPress={onPressVerify}>
            <Text>Verify Email</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

Create the email/password sign-in flow

To authenticate a user using their email and password, you must:

  1. Initiate the sign-in process by collecting the user's email address and password.
  2. Prepare the first factor verification.
  3. Attempt to complete the first factor verification.
  4. If the verification is successful, set the newly created session as the active session.

This example is written for Next.js App Router but it can be adapted for any React meta framework, such as Remix or Gatsby.

app/sign-in/[[...sign-in]]/page.tsx
'use client';

import * as React from 'react';
import { useSignIn } from '@clerk/nextjs';
import { useRouter } from 'next/navigation';

export default function SignInForm() {
  const { isLoaded, signIn, setActive } = useSignIn();
  const [email, setEmail] = React.useState('');
  const [password, setPassword] = React.useState('');
  const router = useRouter();

  // Handle the submission of the sign-in form
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!isLoaded) {
      return;
    }

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: email,
        password,
      });

      // If sign-in process is complete, set the created session as active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });
        router.push('/');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, 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));
    }
  };

  // Display a form to capture the user's email and password
  return (
    <>
      <h1>Sign in</h1>
      <form onSubmit={(e) => handleSubmit(e)}>
        <div>
          <label htmlFor="email">Enter email address</label>
          <input
            onChange={(e) => setEmail(e.target.value)}
            id="email"
            name="email"
            type="email"
            value={email}
          />
        </div>
        <div>
          <label htmlFor="password">Enter password</label>
          <input
            onChange={(e) => setPassword(e.target.value)}
            id="password"
            name="password"
            type="password"
            value={password}
          />
        </div>
        <button type="submit">Sign in</button>
      </form>
    </>
  );
}

For the following example, your HTML file should look like this:

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-in">
      <h2>Sign in</h2>
      <form id="sign-in-form">
        <label for="email">Enter email address</label>
        <input name="email" id="sign-in-email" />
        <label for="password">Enter password</label>
        <input name="password" id="sign-in-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <script
      type="module"
      src="/src/main.js"
      async
      crossorigin="anonymous"
    ></script>
  </body>
</html>

And your JavaScript file should look like this:

main.js
import { Clerk } from '@clerk/clerk-js';

// Initialize Clerk with your Clerk publishable key
const clerk = new Clerk('YOUR_PUBLISHABLE_KEY');
await clerk.load();

if (clerk.user) {
  // Mount user button component
  document.getElementById('signed-in').innerHTML = `
    <div id="user-button"></div>
  `;

  const userbuttonDiv = document.getElementById('user-button');

  clerk.mountUserButton(userbuttonDiv);
} else {
  // Handle the sign-in form
  document
    .getElementById('sign-in-form')
    .addEventListener('submit', async (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const emailAddress = formData.get('email');
      const password = formData.get('password');

      try {
        // Start the sign-in process
        const signInAttempt = await clerk.client.signIn.create({
          identifier: emailAddress,
          password,
        });

        // If the sign-in is complete, set the user as active
        if (signInAttempt.status === 'complete') {
          await clerk.setActive({ session: signInAttempt.createdSessionId });

          location.reload();
        } else {
          // If the status is not complete, check why. User may need to
          // complete further steps.
          console.error(JSON.stringify(signInAttempt, null, 2));
        }
      } catch (error) {
        // See https://clerk.com/docs/custom-flows/error-handling
        // for more info on error handling
        console.error(error);
      }
    });
}
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Clerk + JavaScript App</title>
  </head>
  <body>
    <div id="signed-in"></div>

    <div id="sign-in">
      <h2>Sign in</h2>
      <form id="sign-in-form">
        <label for="email">Enter email address</label>
        <input name="email" id="sign-in-email" />
        <label for="password">Enter password</label>
        <input name="password" id="sign-in-password" />
        <button type="submit">Continue</button>
      </form>
    </div>

    <script
      async
      crossorigin="anonymous"
      data-clerk-publishable-key="YOUR_PUBLISHABLE_KEY"
      src="https://YOUR_FRONTEND_API_URL/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
      type="text/javascript"
    ></script>

    <script>
      window.addEventListener('load', async function () {
        await Clerk.load();

        if (Clerk.user) {
          // Mount user button component
          document.getElementById('signed-in').innerHTML = `
            <div id="user-button"></div>
          `;

          const userbuttonDiv = document.getElementById('user-button');

          Clerk.mountUserButton(userbuttonDiv);
        } else {
          // Handle the sign-in form
          document.getElementById('sign-in-form')
            .addEventListener('submit', async (e) => {
              e.preventDefault();

              const formData = new FormData(e.target);
              const emailAddress = formData.get('email');
              const password = formData.get('password');

              try {
                // Start the sign-in process
                const signInAttempt = await Clerk.client.signIn.create({
                  identifier: emailAddress,
                  password,
                });

                // If the sign-in is complete, set the user as active
                if (signInAttempt.status === 'complete') {
                  await Clerk.setActive({ session: signInAttempt.createdSessionId });

                  location.reload();
                } else {
                  // If the status is not complete, check why. User may need to
                  // complete further steps.
                  console.error(JSON.stringify(signInAttempt, null, 2));
                }
              } catch (error) {
                // See https://clerk.com/docs/custom-flows/error-handling
                // for more info on error handling
                console.error(error);
              }
            });
        }
      });
    </script>
  </body>
</html>

First create a route group with a layout that redirects users if they're already signed in, as shown in the following example:

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

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

  if (isSignedIn) {
    return <Redirect href={"/dashboard"} />;
  }

  return <Stack />;
}

Then create a sign-in page that allows users to sign in or navigate to the sign-up page.

/app/(auth)/sign-in.tsx
import { useClerk, useSignIn } from "@clerk/clerk-expo";
import { Link, Stack } from "expo-router";
import { Text, TextInput, TouchableOpacity, View } from "react-native";
import React from "react";

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

  const [emailAddress, setEmailAddress] = React.useState("");
  const [password, setPassword] = React.useState("");

  // Handle the submission of the sign-in form
  const onSignInPress = React.useCallback(async () => {
    if (!isLoaded) {
      return;
    }

    // Start the sign-in process using the email and password provided
    try {
      const signInAttempt = await signIn.create({
        identifier: emailAddress,
        password,
      });

      // If sign-in process is complete, set the created session as active
      // and redirect the user
      if (signInAttempt.status === 'complete') {
        await setActive({ session: signInAttempt.createdSessionId });
        router.replace('/dashboard');
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, 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, emailAddress, password]);

  return (
    <View>
      <Stack.Screen
        options={{
          title: "Sign In",
        }}
      />
      <View>
        <TextInput
          autoCapitalize="none"
          value={emailAddress}
          placeholder="Email..."
          placeholderTextColor="#000"
          onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
        />
      </View>

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

      <TouchableOpacity onPress={onSignInPress}>
        <Text>Sign in</Text>
      </TouchableOpacity>

      <View>
        <Text>Don't have an account?</Text>

        <Link href="/sign-up" asChild>
          <TouchableOpacity>
            <Text>Sign up</Text>
          </TouchableOpacity>
        </Link>
      </View>
    </View>
  );
}

Feedback

What did you think of this content?