back to writing

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!

Get the next post in your inbox

Plus three free e-books. No spam, unsubscribe any time.