Building a Robust Email Authentication System with Firebase, React and TypeScript
Introduction:
Welcome to this comprehensive Firebase Authentication tutorial, where we'll guide you through creating a robust email authentication system for your web application. Firebase offers a powerful suite of tools for building seamless authentication experiences, and by the end of this tutorial, you'll have a fully functional system in place.
Overview
In this tutorial, we'll cover:
- Setting up Firebase and obtaining necessary credentials.
- Implementing email authentication with Firebase.
- Handling password authentication.
- Managing user sessions, sign out, and deletion.
- Error handling to ensure a smooth user experience.
You can follow along with the written instructions below or watch the accompanying Youtube Video.
Prerequisites
Before we begin, make sure you have:
- A Firebase account. If you don't have one, you can sign up here.
- Node.js and npm installed on your machine.
Getting Started
Let's dive into the implementation steps:
Step 1: Firebase Setup
Create a Firebase account if you haven't already. Enable email authentication in the Firebase console. Obtain your Firebase configuration details.
Step 2: Setting up the Project
Clone the project repository from GitHub or use the final code from here.
Install Firebase in your project:
npm install firebase
Create a new folder firebase in the src/lib directory and add an index.ts file. This file will contain the Firebase configuration and initialization code.
// src/lib/firebase/index.ts
import { getApp, getApps, initializeApp } from "firebase/app";
import { GoogleAuthProvider, getAuth } from "firebase/auth";
// Your web app's Firebase configuration
const firebaseConfig = {
// Your Firebase config details here you should get them when you sign up
};
const firestoreApp = getApps().length
? getApp()
: initializeApp(firebaseConfig);
const googleAuthProvider = new GoogleAuthProvider();
const auth = getAuth(firestoreApp);
export { auth, googleAuthProvider };
Step 3: Email Authentication Helpers
Create an Authentication folder within the firebase directory. Inside this folder, add an index.ts file to handle email authentication helper functions.
// src/lib/firebase/Authentication/index.ts
import {
EmailAuthProvider,
createUserWithEmailAndPassword,
reauthenticateWithCredential,
sendEmailVerification,
signInWithEmailAndPassword,
updateEmail,
} from "firebase/auth";
import { auth } from "../..";
import { RoutesEnum } from "../../../../routes";
import { NavigateFunction } from "react-router-dom";
import { FirebaseError } from "firebase/app";
import { generateFirebaseAuthErrorMessage } from "../ErrorHandler";
export const registerUser = async (
name: string,
email: string,
password: string,
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
navigate: NavigateFunction
) => {
try {
setLoading(true);
// create a new user
const userCredential = await createUserWithEmailAndPassword(
auth,
email,
password
);
const results = userCredential.user;
console.log(results);
// Send an email verification to the users email
await sendEmailVerification(results);
alert(
`A verification email has been sent to your email address ${name}!. Please verify your email to login.`
);
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
}
console.error(error);
} finally {
setLoading(false);
navigate(RoutesEnum.Login);
}
};
export const loginUserWithEmailAndPassword = async (
email: string,
password: string,
navigate: NavigateFunction
) => {
try {
console.log(email, password);
// Login user
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password
);
const results = userCredential.user;
if (results.emailVerified === false) {
alert("Please verify your email to login.");
return;
}
navigate(RoutesEnum.Home);
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
}
console.error(error);
}
};
export const updateUserEmail = async (
email: string,
newEmail: string,
password: string,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
) => {
try {
if (auth.currentUser === null) return;
setIsLoading(true);
// Reauthenticate the user before updating the email
const credential = EmailAuthProvider.credential(email, password);
await reauthenticateWithCredential(auth.currentUser, credential);
// Update the email after successful reauthentication
await updateEmail(auth.currentUser, newEmail);
// Send email verification to the new email
await sendEmailVerification(auth.currentUser);
alert(
`A verification email has been sent to your new email address ${newEmail}!. Please verify your email to login.`
);
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
}
console.error(error);
} finally {
setIsLoading(false);
}
};
Step 4: Password Authentication Helpers
Similarly, create an index.ts file in the PasswordAuthentication folder within the Authentication directory to handle password authentication.
// src/lib/firebase/Authentication/PasswordAuthentication/index.ts
import {
sendPasswordResetEmail,
EmailAuthProvider,
reauthenticateWithCredential,
updatePassword,
} from "firebase/auth";
import { NavigateFunction } from "react-router-dom";
import { auth } from "../..";
import { RoutesEnum } from "../../../../routes";
import { generateFirebaseAuthErrorMessage } from "../ErrorHandler";
import { FirebaseError } from "firebase/app";
export const forgotPassword = async (
email: string,
navigate: NavigateFunction
) => {
try {
// check email exist or not
if (email === "") {
alert("Please enter your email address!");
return;
}
// send password reset email
await sendPasswordResetEmail(auth, email);
navigate(RoutesEnum.Login);
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
}
console.error(error);
}
};
export const updateUserPassword = async (
currentPassword: string,
newPassword: string,
navigate: NavigateFunction,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>
) => {
try {
// check if valid user
const user = auth.currentUser;
if (!user) return;
// check if current password is valid
if (
!currentPassword ||
currentPassword === "" ||
currentPassword.length < 6
) {
alert("Please enter your current password");
return;
}
// check if new password is valid
if (!newPassword || newPassword === "") {
alert("Please enter your new password");
return;
}
setIsLoading(true);
// validate old password
const credential = EmailAuthProvider.credential(
user.email as string,
currentPassword
);
// reauthenticate user
await reauthenticateWithCredential(user, credential);
// update password
await updatePassword(user, newPassword);
navigate(RoutesEnum.Login);
alert("Password updated successfully");
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
}
console.error(error);
}
};
Step 5: User Management
Implement functions for signing out users and deleting users from Firestore.
// src/lib/firebase/Authentication/UserManagement/index.ts
// Sign out
import { signOut } from "firebase/auth";
import { auth } from "../..";
import { RoutesEnum } from "../../../../routes";
import { NavigateFunction } from "react-router-dom";
export const signOutUser = async (navigate: NavigateFunction) => {
try {
await signOut(auth);
alert("You have been signed out.");
navigate(RoutesEnum.Home);
} catch (error) {
console.error(error);
}
};
// Delete User
import { FirebaseError } from "firebase/app";
import { auth } from "../..";
import { generateFirebaseAuthErrorMessage } from "../ErrorHandler";
import {
EmailAuthProvider,
GoogleAuthProvider,
deleteUser,
reauthenticateWithCredential,
reauthenticateWithPopup,
} from "firebase/auth";
import { RoutesEnum } from "../../../../routes";
import { NavigateFunction } from "react-router-dom";
export const deleteUserFromFirestore = async (
navigate: NavigateFunction,
isEmailUser: boolean,
isGoogleUser: boolean,
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>,
password?: string
) => {
const user = auth?.currentUser;
if (!user || user === null) return;
try {
setIsLoading(true);
// handle google user
if (isGoogleUser) {
const googleProvider = new GoogleAuthProvider();
await reauthenticateWithPopup(user, googleProvider);
await deleteUser(user);
navigate(RoutesEnum.Home);
}
// handle email user
if (isEmailUser) {
if (!password || password === "") {
alert("Please enter your password");
return;
}
const userEmail = user.email as string;
const credential = EmailAuthProvider.credential(userEmail, password);
await reauthenticateWithCredential(user, credential);
await deleteUser(user);
navigate(RoutesEnum.Home);
}
} catch (error) {
if (error instanceof FirebaseError) {
generateFirebaseAuthErrorMessage(error);
return;
}
} finally {
setIsLoading(false);
}
};
Step 6: Error Handling
To handle various Firebase authentication errors, create an ErrorHandler.ts file within the firebase directory.
// src/lib/firebase/ErrorHandler.ts
import { FirebaseError } from "firebase/app";
// https://firebase.google.com/docs/auth/admin/errors
export const generateFirebaseAuthErrorMessage = (error: FirebaseError) => {
switch (error?.code) {
case "auth/invalid-email":
alert("Invalid email address. Please enter a valid email.");
break;
case "auth/user-not-found":
alert("User not found. Please check the email address.");
break;
case "auth/wrong-password":
alert("Incorrect password. Please try again.");
break;
case "auth/email-already-in-use":
alert("Email already in use. Please try another email.");
break;
case "auth/weak-password":
alert("Password should be at least 6 characters.");
break;
case "auth/operation-not-allowed":
alert("Operation not allowed. Please try again later.");
break;
case "auth/invalid-verification-code":
alert("Invalid verification code. Please try again.");
break;
case "auth/invalid-verification-id":
alert("Invalid verification ID. Please try again.");
break;
case "auth/code-expired":
alert("Code expired. Please try again.");
break;
case "auth/invalid-action-code":
alert("Invalid action code. Please try again.");
break;
case "auth/user-disabled":
alert("User disabled. Please contact support.");
break;
case "auth/invalid-credential":
alert("Invalid credential. Please try again.");
break;
case "auth/invalid-continue-uri":
alert("Invalid continue URL. Please try again.");
break;
case "auth/unauthorized-continue-uri":
alert("Unauthorized continue URL. Please try again.");
break;
case "auth/missing-continue-uri":
alert("Missing continue URL. Please try again.");
break;
case "auth/missing-verification-code":
alert("Missing verification code. Please try again.");
break;
case "auth/missing-verification-id":
alert("Missing verification ID. Please try again.");
break;
case "auth/captcha-check-failed":
alert("Captcha check failed. Please try again.");
break;
case "auth/invalid-phone-number":
alert("Invalid phone number. Please try again.");
break;
case "auth/missing-phone-number":
alert("Missing phone number. Please try again.");
break;
case "auth/quota-exceeded":
alert("Quota exceeded. Please try again.");
break;
case "auth/missing-app-credential":
alert("Missing app credential. Please try again.");
break;
case "auth/invalid-app-credential":
alert("Invalid app credential. Please try again.");
break;
case "auth/session-expired":
alert("Session expired. Please try again.");
break;
case "auth/missing-or-invalid-nonce":
alert("Missing or invalid nonce. Please try again.");
break;
case "auth/missing-client-identifier":
alert("Missing client identifier. Please try again.");
break;
case "auth/key-retrieval-failed":
alert("Key retrieval failed. Please try again.");
break;
case "auth/invalid-oauth-provider":
alert("Invalid OAuth provider. Please try again.");
break;
case "auth/invalid-oauth-client-id":
alert("Invalid OAuth client ID. Please try again.");
break;
case "auth/invalid-cert-hash":
alert("Invalid cert hash. Please try again.");
break;
case "auth/invalid-user-token":
alert("Invalid user token. Please try again.");
break;
case "auth/invalid-custom-token":
alert("Invalid custom token. Please try again.");
break;
case "auth/app-deleted":
alert("App deleted. Please try again.");
break;
case "auth/app-not-authorized":
alert("App not authorized. Please try again.");
break;
case "auth/argument-error":
alert("Argument error. Please try again.");
break;
case "auth/invalid-api-key":
alert("Invalid API key. Please try again.");
break;
case "auth/network-request-failed":
alert("Network request failed. Please try again.");
break;
case "auth/requires-recent-login":
alert("Requires recent login. Please try again.");
break;
case "auth/too-many-requests":
alert("Too many requests. Please try again.");
break;
case "auth/unauthorized-domain":
alert("Unauthorized domain. Please try again.");
break;
case "auth/user-token-expired":
alert("User token expired. Please try again.");
break;
case "auth/web-storage-unsupported":
alert("Web storage unsupported. Please try again.");
break;
case "auth/account-exists-with-different-credential":
alert("Account exists with different credential. Please try again.");
break;
case "auth/auth-domain-config-required":
alert("Auth domain config required. Please try again.");
break;
case "auth/cancelled-popup-request":
alert("Cancelled popup request. Please try again.");
break;
case "auth/credential-already-in-use":
alert("Credential already in use. Please try again.");
break;
case "auth/custom-token-mismatch":
alert("Custom token mismatch. Please try again.");
break;
case "auth/provider-already-linked":
alert("Provider already linked. Please try again.");
break;
case "auth/timeout":
alert("Timeout. Please try again.");
break;
case "auth/missing-android-pkg-name":
alert("Missing Android package name. Please try again.");
break;
case "auth/missing-ios-bundle-id":
alert("Missing iOS bundle ID. Please try again.");
break;
case "auth/invalid-dynamic-link-domain":
alert("Invalid dynamic link domain. Please try again.");
break;
case "auth/invalid-persistence-type":
alert("Invalid persistence type. Please try again.");
break;
case "auth/unsupported-persistence-type":
alert("Unsupported persistence type. Please try again.");
break;
case "auth/invalid-oauth-client-secret":
alert("Invalid OAuth client secret. Please try again.");
break;
case "auth/invalid-argument":
alert("Invalid argument. Please try again.");
break;
case "auth/invalid-creation-time":
alert("Invalid creation time. Please try again.");
break;
case "auth/invalid-disabled-field":
alert("Invalid disabled field. Please try again.");
break;
case "auth/invalid-display-name":
alert("Invalid display name. Please try again.");
break;
case "auth/invalid-email-verified":
alert("Invalid email verified. Please try again.");
break;
case "auth/invalid-hash-algorithm":
alert("Invalid hash algorithm. Please try again.");
break;
case "auth/invalid-hash-block-size":
alert("Invalid hash block size. Please try again.");
break;
case "auth/invalid-hash-derived-key-length":
alert("Invalid hash derived key length. Please try again.");
break;
case "auth/invalid-hash-key":
alert("Invalid hash key. Please try again.");
break;
case "auth/invalid-hash-memory-cost":
alert("Invalid hash memory cost. Please try again.");
break;
case "auth/invalid-hash-parallelization":
alert("Invalid hash parallelization. Please try again.");
break;
case "auth/invalid-hash-rounds":
alert("Invalid hash rounds. Please try again.");
break;
case "auth/invalid-hash-salt-separator":
alert("Invalid hash salt separator. Please try again.");
break;
case "auth/invalid-id-token":
alert("Invalid ID token. Please try again.");
break;
case "auth/invalid-last-sign-in-time":
alert("Invalid last sign in time. Please try again.");
break;
case "auth/invalid-page-token":
alert("Invalid page token. Please try again.");
break;
case "auth/invalid-password":
alert("Invalid password. Please try again.");
break;
case "auth/invalid-password-hash":
alert("Invalid password hash. Please try again.");
break;
case "auth/invalid-password-salt":
alert("Invalid password salt. Please try again.");
break;
case "auth/invalid-photo-url":
alert("Invalid photo URL. Please try again.");
break;
case "auth/invalid-provider-id":
alert("Invalid provider ID. Please try again.");
break;
case "auth/invalid-session-cookie-duration":
alert("Invalid session cookie duration. Please try again.");
break;
case "auth/invalid-uid":
alert("Invalid UID. Please try again.");
break;
case "auth/invalid-user-import":
alert("Invalid user import. Please try again.");
break;
case "auth/invalid-provider-data":
alert("Invalid provider data. Please try again.");
break;
case "auth/maximum-user-count-exceeded":
alert("Maximum user count exceeded. Please try again.");
break;
case "auth/missing-hash-algorithm":
alert("Missing hash algorithm. Please try again.");
break;
case "auth/missing-uid":
alert("Missing UID. Please try again.");
break;
case "auth/reserved-claims":
alert("Reserved claims. Please try again.");
break;
case "auth/session-cookie-revoked":
alert("Session cookie revoked. Please try again.");
break;
case "auth/uid-already-exists":
alert("UID already exists. Please try again.");
break;
case "auth/email-already-exists":
alert("Email already exists. Please try again.");
break;
case "auth/phone-number-already-exists":
alert("Phone number already exists. Please try again.");
break;
case "auth/project-not-found":
alert("Project not found. Please try again.");
break;
case "auth/insufficient-permission":
alert("Insufficient permission. Please try again.");
break;
case "auth/internal-error":
alert("Internal error. Please try again.");
break;
default:
alert("Oops! Something went wrong. Please try again later.");
}
};
Conclusion
Congratulations! You've now built a comprehensive email authentication system using Firebase for your web application. With Firebase's powerful authentication features and the functions we've implemented, you can provide a seamless and secure user experience.
See more blogs on my site also or check out videos on the Imran Codes Youtube Channel!