Docs

Build a custom email/password authentication flow

Warning

This guide is for users who want to build a custom user interface using the Clerk API. To use a prebuilt UI, use the 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. In the Clerk Dashboard, navigate to the Email, phone, username page.
  2. Ensure that only Email address is required. If Phone number and Username are enabled, ensure they are not required. Use the settings icon next to each option to verify if a setting is required or optional. If you would like to require Username, you must collect the username and pass it to the create() method in your custom flow.
  3. In the Authentication strategies section of this page, ensure Password is enabled.

Note

By default, the verification method for the Email address strategy is set to Email verification code. This means that when a user signs up using their email address and password, Clerk sends a one-time code to their email address. The user must then enter this code to verify their email and complete the sign-up process.

Sign-up flow

To sign up a user using their email, password, and email verification code, 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. Collect the one-time code and attempt to complete the email address verification with it.
  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.

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 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.push('/')
      } 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) {
      // 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">Continue</button>
        </div>
      </form>
    </>
  )
}
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>
main.js
import { Clerk } from '@clerk/clerk-js'

const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

const clerk = new Clerk(pubKey)
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 email and password provided
      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 {
      // Use the code the user provided to attempt verification
      const signUpAttempt = await clerk.client.signUp.attemptEmailAddressVerification({
        code,
      })

      // Now that the user is created, set the session to active.
      await clerk.setActive({ session: signUpAttempt.createdSessionId })
    } catch (error) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(error)
    }
  })
}
  1. Create the (auth) route group. This groups your sign-up and sign-in pages.
  2. In the (auth) group, create a _layout.tsx file with the following code. The useAuth() hook is used to access the user's authentication state. If the user's already signed in, they'll be redirected to the home page.
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 />
}

In the (auth) group, create a sign-up.tsx file with the following code. The useSignUp() hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email.

app/(auth)/sign-up.tsx
import * as React from 'react'
import { Text, TextInput, Button, View } from 'react-native'
import { useSignUp } from '@clerk/clerk-expo'
import { Link, useRouter } from 'expo-router'

export default function Page() {
  const { isLoaded, signUp, setActive } = useSignUp()
  const router = useRouter()

  const [emailAddress, setEmailAddress] = React.useState('')
  const [password, setPassword] = React.useState('')
  const [pendingVerification, setPendingVerification] = React.useState(false)
  const [code, setCode] = React.useState('')

  // Handle submission of sign-up form
  const onSignUpPress = async () => {
    if (!isLoaded) return

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

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

      // Set 'pendingVerification' to true to display second form
      // and capture OTP code
      setPendingVerification(true)
    } catch (err) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  // Handle submission of verification form
  const onVerifyPress = async () => {
    if (!isLoaded) return

    try {
      // Use the code the user provided to attempt verification
      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('/')
      } 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) {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      console.error(JSON.stringify(err, null, 2))
    }
  }

  if (pendingVerification) {
    return (
      <>
        <Text>Verify your email</Text>
        <TextInput
          value={code}
          placeholder="Enter your verification code"
          placeholderTextColor="#666666"
          onChangeText={(code) => setCode(code)}
        />
        <Button title="Verify" onPress={onVerifyPress} />
      </>
    )
  }

  return (
    <View>
      <>
        <Text>Sign up</Text>
        <TextInput
          autoCapitalize="none"
          value={emailAddress}
          placeholder="Enter email"
          placeholderTextColor="#666666"
          onChangeText={(email) => setEmailAddress(email)}
        />
        <TextInput
          value={password}
          placeholder="Enter password"
          placeholderTextColor="#666666"
          secureTextEntry={true}
          onChangeText={(password) => setPassword(password)}
        />
        <Button title="Continue" onPress={onSignUpPress} />
        <View style={{ flexDirection: 'row', gap: 4 }}>
          <Text>Have an account?</Text>
          <Link href="/sign-in">
            <Text>Sign in</Text>
          </Link>
        </View>
      </>
    </View>
  )
}
EmailPasswordSignUpView.swift
import SwiftUI
import ClerkSDK

struct EmailPasswordSignUpView: View {
  @State private var email = ""
  @State private var password = ""
  @State private var code = ""
  @State private var isVerifying = false

  var body: some View {
    if isVerifying {
      // Display the verification form to capture the OTP code
      TextField("Enter your verification code", text: $code)
      Button("Verify") {
        Task { await verify(code: code) }
      }
    } else {
      // Display the initial sign-up form to capture the email and password
      TextField("Enter email address", text: $email)
      SecureField("Enter password", text: $password)
      Button("Next") {
        Task { await submit(email: email, password: password) }
      }
    }
  }
}

extension EmailPasswordSignUpView {

  func submit(email: String, password: String) async {
    do {
      // Start the sign-up process using the email and password provided
      let signUp = try await SignUp.create(strategy: .standard(emailAddress: email, password: password))

      // Send the user an email with the verification code
      try await signUp.prepareVerification(strategy: .emailCode)

      // Set 'isVerifying' true to display second form
      // and capture the OTP code
      isVerifying = true
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }

  func verify(code: String) async {
    do {
      // Access the in progress sign up stored on the client
      guard let inProgressSignUp = Clerk.shared.client?.signUp else { return }

      // Use the code the user provided to attempt verification
      let signUp = try await inProgressSignUp.attemptVerification(.emailCode(code: code))

      switch signUp.status {
      case .complete:
        // If verification was completed, navigate the user as needed.
        dump(Clerk.shared.session)
      default:
        // If the status is not complete, check why. User may need to
        // complete further steps.
        dump(signUp.status)
      }
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }
}

Sign-in flow

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

  1. Initiate the sign-in process by creating a SignIn using the email address and password provided.
  2. If the attempt 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.

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>
    </>
  )
}
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>
main.js
import { Clerk } from '@clerk/clerk-js'

const pubKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

const clerk = new Clerk(pubKey)
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)
    }
  })
}

In the (auth) group, create a sign-in.tsx file with the following code. The useSignIn() hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page.

app/(auth)/sign-in.tsx
import { useSignIn } from '@clerk/clerk-expo'
import { Link, useRouter } from 'expo-router'
import { Text, TextInput, Button, View } from 'react-native'
import React from 'react'

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

  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('/')
      } 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) {
      // 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>
      <Text>Sign in</Text>
      <TextInput
        autoCapitalize="none"
        value={emailAddress}
        placeholder="Enter email"
        placeholderTextColor="#666666"
        onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
      />
      <TextInput
        value={password}
        placeholder="Enter password"
        placeholderTextColor="#666666"
        secureTextEntry={true}
        onChangeText={(password) => setPassword(password)}
      />
      <Button title="Sign in" onPress={onSignInPress} />
      <View style={{ flexDirection: 'row', gap: 4 }}>
        <Text>Don't have an account?</Text>
        <Link href="/sign-up">
          <Text>Sign up</Text>
        </Link>
      </View>
    </View>
  )
}
EmailPasswordSignInView.swift
import SwiftUI
import ClerkSDK

struct EmailPasswordSignInView: View {
  @State private var email = ""
  @State private var password = ""

  var body: some View {
    TextField("Enter email address", text: $email)
    SecureField("Enter password", text: $password)
    Button("Sign In") {
      Task { await submit(email: email, password: password) }
    }
  }
}

extension EmailPasswordSignInView {

  func submit(email: String, password: String) async {
    do {
      // Start the sign-in process using the email and password provided
      let signIn = try await SignIn.create(strategy: .identifier(email, password: password))

      switch signIn.status {
      case .complete:
        // If sign-in process is complete, navigate the user as needed.
        dump(Clerk.shared.session)
      default:
        // If the status is not complete, check why. User may need to
        // complete further steps.
        dump(signIn.status)
      }
    } catch {
      // See https://clerk.com/docs/custom-flows/error-handling
      // for more info on error handling
      dump(error)
    }
  }
}

Feedback

What did you think of this content?

Last updated on