Skip to main content
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 ensure they are enabled for your application.

  1. In the Clerk Dashboard, navigate to the User & authentication page.
  2. Enable Sign-up with email and Sign-in with email.
  3. Select the Password tab and enable Sign-up with password. Leave Require a password at sign-up enabled.

Note

By default, Email verification code is enabled for both sign-up and sign-in. This means that when a user signs up using their email address, 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. When the user uses the email address to sign in, they are emailed a one-time code to sign in. If you'd like to use Email verification link instead, see the custom flow for email links.

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.
EmailPasswordSignUpView.swift
import SwiftUI
import Clerk

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/guides/development/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(strategy: .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/guides/development/custom-flows/error-handling
        // for more info on error handling
        dump(error)
    }
    }
}
EmailPasswordSignUpViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.flatMap
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signup.SignUp
import com.clerk.api.signup.attemptVerification
import com.clerk.api.signup.prepareVerification
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch

class EmailPasswordSignUpViewModel : ViewModel() {
private val _uiState =
    MutableStateFlow<UiState>(UiState.Loading)
val uiState = _uiState.asStateFlow()

init {
    combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
        _uiState.value =
        when {
            !isInitialized -> UiState.Loading
            user != null -> UiState.Verified
            else -> UiState.Unverified
        }
    }
    .launchIn(viewModelScope)
}

fun submit(email: String, password: String) {
    viewModelScope.launch {
    SignUp.create(SignUp.CreateParams.Standard(emailAddress = email, password = password))
        .flatMap { it.prepareVerification(SignUp.PrepareVerificationParams.Strategy.EmailCode()) }
        .onSuccess { _uiState.value = UiState.Verifying }
        .onFailure {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        // for more info on error handling
        }
    }
}

fun verify(code: String) {
    val inProgressSignUp = Clerk.signUp ?: return
    viewModelScope.launch {
    inProgressSignUp
        .attemptVerification(SignUp.AttemptVerificationParams.EmailCode(code))
        .onSuccess { _uiState.value = UiState.Verified }
        .onFailure {
        // See https://clerk.com/docs/guides/development/custom-flows/error-handling
        // for more info on error handling
        }
    }
}

sealed interface UiState {
    data object Loading : UiState

    data object Unverified : UiState

    data object Verifying : UiState

    data object Verified : UiState
}
}
EmailPasswordSignUpActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle

class EmailPasswordSignUpActivity : ComponentActivity() {

  val viewModel: EmailPasswordSignUpViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      val state by viewModel.uiState.collectAsStateWithLifecycle()
      EmailPasswordSignInView(
        state = state,
        onSubmit = viewModel::submit,
        onVerify = viewModel::verify,
      )
    }
  }
}

@Composable
fun EmailPasswordSignInView(
  state: EmailPasswordSignUpViewModel.UiState,
  onSubmit: (String, String) -> Unit,
  onVerify: (String) -> Unit,
) {
  var email by remember { mutableStateOf("") }
  var password by remember { mutableStateOf("") }
  var code by remember { mutableStateOf("") }

  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    when (state) {
      EmailPasswordSignUpViewModel.UiState.Unverified -> {
        Column(
          verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
          horizontalAlignment = Alignment.CenterHorizontally,
        ) {
          TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
          TextField(
            value = password,
            onValueChange = { password = it },
            visualTransformation = PasswordVisualTransformation(),
            label = { Text("Password") },
          )
          Button(onClick = { onSubmit(email, password) }) { Text("Next") }
        }
      }
      EmailPasswordSignUpViewModel.UiState.Verified -> {
        Text("Verified!")
      }
      EmailPasswordSignUpViewModel.UiState.Verifying -> {
        Column(
          verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
          horizontalAlignment = Alignment.CenterHorizontally,
        ) {
          TextField(
            value = code,
            onValueChange = { code = it },
            label = { Text("Enter your verification code") },
          )
          Button(onClick = { onVerify(code) }) { Text("Verify") }
        }
      }
      EmailPasswordSignUpViewModel.UiState.Loading -> CircularProgressIndicator()
    }
  }
}

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.
EmailPasswordSignInView.swift
import SwiftUI
import Clerk

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/guides/development/custom-flows/error-handling
        // for more info on error handling
        dump(error)
    }
    }
}
EmailPasswordSignInViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.clerk.api.Clerk
import com.clerk.api.network.serialization.onFailure
import com.clerk.api.network.serialization.onSuccess
import com.clerk.api.signin.SignIn
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch

class EmailPasswordSignInViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(
        UiState.SignedOut
    )
    val uiState = _uiState.asStateFlow()

    init {
        combine(Clerk.userFlow, Clerk.isInitialized) { user, isInitialized ->
            _uiState.value = when {
                !isInitialized -> UiState.Loading
                user == null -> UiState.SignedOut
                else -> UiState.SignedIn
            }
        }.launchIn(viewModelScope)
    }

    fun submit(email: String, password: String) {
        viewModelScope.launch {
            SignIn.create(
                SignIn.CreateParams.Strategy.Password(
                    identifier = email,
                    password = password
                )
            ).onSuccess {
                _uiState.value = UiState.SignedIn
            }.onFailure {
                // See https://clerk.com/docs/guides/development/custom-flows/error-handling
                // for more info on error handling
            }
        }
    }


    sealed interface UiState {
        data object Loading : UiState

        data object SignedOut : UiState

        data object SignedIn : UiState
    }
}
EmailPasswordSignInActivity.kt
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.*
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.clerk.api.Clerk

class EmailPasswordSignInActivity : ComponentActivity() {

    val viewModel: EmailPasswordSignInViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val state by viewModel.uiState.collectAsStateWithLifecycle()
            EmailPasswordSignInView(
                state = state,
                onSubmit = viewModel::submit
            )
        }
    }
}

@Composable
fun EmailPasswordSignInView(
    state: EmailPasswordSignInViewModel.UiState,
    onSubmit: (String, String) -> Unit,
) {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

        when (state) {

            EmailPasswordSignInViewModel.UiState.SignedOut -> {
                Column(
                    verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    TextField(value = email, onValueChange = { email = it }, label = { Text("Email") })
                    TextField(
                        value = password,
                        onValueChange = { password = it },
                        visualTransformation = PasswordVisualTransformation(),
                        label = { Text("Password") },
                    )
                    Button(onClick = { onSubmit(email, password) }) { Text("Sign in") }
                }
            }

            EmailPasswordSignInViewModel.UiState.SignedIn -> {
                Text("Current session: ${Clerk.session?.id}")
            }

            EmailPasswordSignInViewModel.UiState.Loading ->
                Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    CircularProgressIndicator()
                }
        }
    }
}

Feedback

What did you think of this content?

Last updated on