Build a custom email/password authentication flow
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.
- In the Clerk Dashboard, navigate to the User & authentication page.
- Enable Sign-up with email and Sign-in with email.
- Select the Password tab and enable Sign-up with password. Leave Require a password at sign-up enabled.
Sign-up flow
To sign up a user using their email, password, and email verification code, you must:
- Initiate the sign-up process by collecting the user's email address and password.
- Prepare the email address verification, which sends a one-time code to the given address.
- Collect the one-time code and attempt to complete the email address verification with it.
- If the email address verification is successful, set the newly created session as the active session.
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)
}
}
}
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
}
}
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:
- Initiate the sign-in process by creating a
SignIn
using the email address and password provided. - If the attempt is successful, set the newly created session as the active session.
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)
}
}
}
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
}
}
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
Last updated on