Sign in with Email & Password Authentication Flow
Apps generated with AuthKit include a complete authentication flow for signing in with email and password. This flow includes:
- Signing In - The user can sign in with their email and password.
- Signing Out - The user can sign out of their account.
- Account Creation - An account creation screen with password strength indicator
- Email Verification After account creation, the user will receive an email to verify their email address
- Password Reset / Forgot Password - The user can reset their password pressing on the forgot password button, after which they will receive an email to reset their password
- Account Deletion - The user can delete their account
Signing In & Signing Out
If the user is not signed in and tries to access features that require authentication, they will be presented with a sign in sheet, that will prompt them to sign in. After entering the email and password, and pressing the sign in button, the user will be signed in (if their credentials are correct).
Demo
Walkthrough
In the demo, the user is not signed in and tries to open the account settings, which requires the user to be signed in (Lock SwiftUI Views to Signed-In Users).
When presented with the SignInView
, the user has the option to sign in with their email and password, reset their password, create a new account, or sign in with Apple.
In this case, the user signs in with their email and password, and is then able to access the account settings. After the user enters their email and password,
the app calls the signIn()
function of the shared DB
environment object, which signs the user in.
If there was an error, it is handled by showing the user a corresponding in-app notification.
If the sign in was successful, the sign in sheet will be hidden.
The sign in state is automatically saved in the DB
object and can be access through the authState
variable.
// ...
public enum AuthState {
case signedOut
case signedInUnverified // means user is signed in but hasn't verified his email address yet
case signedIn
}
// ...
@MainActor
public class DB: ObservableObject {
// ...
@Published public var authState: AuthState = .signedOut
// ...
}
// ...
SignInView
// ...
public struct SignInView: View {
@ObservedObject var db: DB
// ...
public var body: some View {
NavigationStack(path: $signInFlowPath) {
// ...
VStack {
// ... sign in hero section
EmailInputFields(
forgotPasswordAction: { /* ... */ },
continueAction: { email, password in
// ...
try await db.signIn(email: email, password: password)
// ...
})
// ... sign up button + sign in with apple button
}
// ...
}
// ...
}
}
The signIn()
function of the DB
object
This function takes two parameters: the email and password of the user. It is an asynchronous function that throws an error if the sign in fails.
extension DB {
public func signIn(email: String, password: String) async throws { }
}
Account Creation & Email Verification
To create an account using email and password, the user has to press Sign Up using Email on the sign in sheet. This will redirect the user to the
SignUpView
, where they can enter their email and password. The password field includes a password strength indicator below. After entering their email and password,
the user can press the sign up button to create an account. This calls the signUp()
function of the DB
object. This function creates a new user with a randomly generated username,
and sends a verification email to the user. The user will then be redirected to the VerifyEmailView
.
The sign in process in not complete yet. Every time the user re-opens the app, and the sign-in view is shown, it automatically redirects the user to the VerifyEmailView
if the user hasn't verified their email address yet. Because the user will probably minimize the app to verify their email, the app will check the verification
state every time the app will come to the foreground.
To prevent abuse, we require the user to have their email verified. Meaning,
that even if the user tries to log in using another device with the same
credentials as before, they will be "logged in" and straight away redirected
to the VerifyEmailView
. The authentication state in
authState
will be set to .signedInUnverified
and should be
treated as if they are not signed in.
Demo
SignInView
// ...
public struct SignInView: View {
// ...
@ObservedObject var db: DB
@State var signInFlowPath: NavigationPath
// ...
public init(/* ... */) {
// ...
var navPath = NavigationPath()
// if the user is signed in, but hasn't verified their email
// address yet, redirect them to the verify email view
if let user = db.currentUser {
if db.currentUserProvider == .email && !user.isEmailVerified {
navPath.append(SignInFlowPath.confirmEmail)
} else {
// ... user signed in, hide the sign in sheet
}
}
self.signInFlowPath = navPath
}
public var body: some View {
NavigationStack(path: $signInFlowPath) {
ScrollView {
VStack {
// ... sign in hero section
// ... sign in fields
// ...
SignUpButtons(shouldShowEmailSignUpScreen: {
signInFlowPath.append(SignInFlowPath.emailSignUp)
})
// ...
}
// ...
.navigationDestination(for: SignInFlowPath.self) { authScreen in
switch authScreen {
case .emailSignUp:
SignUpView()
.environmentObject(db)
case .confirmEmail:
VerifyEmailView()
.environmentObject(db)
.navigationBarBackButtonHidden(true)
case .forgotPassword:
// ...
}
}
// ...
}
}
}
}
SignUpView
struct SignUpView: View {
@EnvironmentObject var db: DB
var body: some View {
VStack {
// ... hero view
EmailInputFields { email, password in
// ...
try await db.signUp(email: email, password: password)
// ...
}
}
// ...
}
}
VerifyEmailView
struct VerifyEmailView: View {
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var db: DB
@StateObject var verificationTimer = VerificationTimer()
public var body: some View {
if let user = db.currentUser, let email = user.email {
VStack {
// ... hero view
// allow the user to resend the verification email when the timer runs out
Button(
"Resend Verification Email\(verificationTimer.secondsLeft > 0 ? " (\(verificationTimer.secondsLeft))" : "")"
) {
verificationTimer.startCountdown()
// ...
try await db.sendVerificationEmail()
//...
}
.disabled(verificationTimer.secondsLeft > 0)
// ...
// ... sign out button
}
// ...
// app in foreground -> refresh user data to check verification state
.onChange(of: scenePhase) {
if scenePhase == .active {
Task {
await db.refreshUserData()
}
}
}
// ...
} else {
// ... invalid state
}
}
}
The signUp()
function of the DB
object
This function takes two parameters: the email and password of the user. It is an asynchronous function that throws an error if the sign in fails. Will create a new user in the Firebase Authentication service, give a new new to the user and send a verification email.
extension DB {
public func signUp(email: String, password: String) async throws {
// ...
do {
let result = try await Auth.auth().createUser(withEmail: email, password: password)
// ...
try await newDisplayName(randomNicknameGenerator()) // will update the current user's display name
do {
try await sendVerificationEmail()
} catch {
// ... handle error
}
// ...
} catch {
// ... handle error
}
}
}
Password Reset / Forgot Password
If the user forgets their password, but remembers their email, they have the option to press the "Forgot Password" icon on the sign in sheet.
This will redirect them to the ForgotPasswordView
, where they can enter their email. After entering their email address, they will receive an email with a link to reset their password.
This opens webpage served by Firebase, where the user can enter a new password. When the users opens the app, they have the option to go back and try to sign in again.
Demo
SignInView
public struct SignInView: View {
@ObservedObject var db: DB
// ...
public var body: some View {
NavigationStack(path: $signInFlowPath) {
ScrollView {
VStack {
// ... sign in hero section
EmailInputFields(
forgotPasswordAction: {
signInFlowPath.append(SignInFlowPath.forgotPassword)
},
continueAction: { email, password in
// ...
})
// ... sign up button + sign in with apple button
}
// ...
.navigationDestination(for: SignInFlowPath.self) { authScreen in
switch authScreen {
case .emailSignUp:
// ...
case .confirmEmail:
// ...
case .forgotPassword:
ForgotPasswordView {
signInFlowPath.removeLast(signInFlowPath.count) // on cancel, go back to sign in view
}
.environmentObject(db)
.navigationBarBackButtonHidden(true)
}
}
// ...
}
}
// ...
}
}
ForgotPasswordView
struct ForgotPasswordView: View {
@EnvironmentObject var db: DB
// ...
@State var email: String
// ...
@State private var emailSent = false
// ...
public var body: some View {
VStack {
// ... hero view
TextField("Your Account Email", text: $email)
// ...
Button(emailSent ? "Request Sent" : "Send Request") {
// ... validate email
Task {
emailSent = true
// ...
try await db.requestPasswordReset(email: email)
// ...
}
}
// ...
.disabled(emailSent)
// ...
}
// ...
}
}
The requestPasswordReset()
function of the DB
object
Sends a password reset email to the passed email address.
extension DB {
public func requestPasswordReset(email: String) async throws {
// ...
do {
// ...
try await Auth.auth().sendPasswordReset(withEmail: email)
// ...
} catch {
// ... handle error
}
}
}
Account Deletion
In contrast to other actions, account deletion doesn't happen on the SignInView
, but rather in the account settings (AccountSettingsView
).
The user can press the "Delete Account" button in the account settings, which requires an additional confirmation to prevent accidental deletion.
After that, a reauthentication sheet is shown, prompting the user to enter their current password to confirm the identity.
On successful reauthentication, the user is signed out and their account is deleted.
Demo
AccountSettingsView
public struct AccountSettingsView: View {
// ...
@EnvironmentObject var db: DB
@State private var showAccountDeleteDialog = false
@State private var reAuthSheetRef = false
// ...
public var body: some View {
List {
// ... account settings header (image, name, email)
Section {
// ... change name button
// ... photos picker
// ... change password button
Button("Delete Account", role: .destructive) {
// ...
showAccountDeleteDialog = true
}
.confirmationDialog(
"Are you sure you want to delete your Account?", isPresented: $showAccountDeleteDialog
) {
Button("Confirm Account Deletion", role: .destructive) {
// show sheet to re-authenticate
showReAuthSheet(db: db, reAuthSheetRef: $reAuthSheetRef) { result in
switch result {
case .success: // on success, hide re-auth sheet and show password sheet after delay
// ...
await Task.sleep(for: .seconds(0.5))
await db.deleteUser()
// ...
dismissReAuthSheet(reAuthSheetRef: $reAuthSheetRef)
case .canceled: // on cancel, hide re-auth sheet
dismissReAuthSheet(reAuthSheetRef: $reAuthSheetRef)
case .forgotPassword:
// ... show password reset sheet
}
}
}
}
// ... sign out button
}
}
// ...
}
// ...
}
The deleteUser()
function of the DB
object
This function deletes the current user's account and logs the user out. It is an asynchronous function that throws an error if the deletion fails.
extension DB {
public func deleteUser() async throws {
// ...
do {
// ...
try await currentUser.delete()
// ...
try signOut()
// ...
} catch {
// ... error handling
}
}
}