Building a Robust Email Authentication System with Firebase, React and TypeScript

Firebase Auth

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:

  1. Setting up Firebase and obtaining necessary credentials.
  2. Implementing email authentication with Firebase.
  3. Handling password authentication.
  4. Managing user sessions, sign out, and deletion.
  5. 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!