Building a Firebase Authentication and Private Route System in a React App

Building a Firebase Authentication and Private Route System in a React App

Firebase Authentication is a robust and secure solution to handle user authentication in web applications. It offers various authentication methods, including email and password, Google Sign-In, and more. In this article, we'll walk through building a Firebase Authentication system in a React application. We'll provide you with the necessary code snippets and guide you through the process.

Prerequisites

Before we get started, make sure you have the following:

  1. Node.js and npm installed on your machine.

  2. A Firebase project. You can create one by visiting the Firebase Console.

  3. Firebase SDK installed in your project. You can add it using npm install firebase.

Setting Up Firebase

First, initialize Firebase in your project by creating a Firebase configuration file. Replace the placeholders with your Firebase project details but make sure to keep your API keys secure.

// FirebaseConfig.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

export default auth;

Creating the Authentication Provider

Next, let's create an AuthProvider component that will provide authentication-related functionality to our app.

// AuthProvider.js
import {
  createUserWithEmailAndPassword,
  onAuthStateChanged,
  signInWithEmailAndPassword,
  signOut,
} from "firebase/auth";
import { createContext, useEffect, useState } from "react";
import PropTypes from "prop-types";
import auth from "./FirebaseConfig";

export const AuthContext = createContext(null);

const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  const createUser = (email, password) => {
    setLoading(true);
    return createUserWithEmailAndPassword(auth, email, password);
  };

  const loginUser = (email, password) => {
    setLoading(true);
    return signInWithEmailAndPassword(auth, email, password);
  };

  const logOut = () => {
    setLoading(true);
    return signOut(auth);
  };

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });

    return () => {
      unsubscribe();
    };
  }, []);

  const authValue = {
    createUser,
    user,
    loginUser,
    logOut,
    loading,
  };

  return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
};

AuthProvider.propTypes = {
  children: PropTypes.node.isRequired,
};

export default AuthProvider;

Building the User Interface

Now that we have our authentication logic set up, let's create the user interface components. We'll create components for login, signup, profile, and a private route.

Header Component

The Header component is responsible for rendering the navigation bar at the top of the application. It handles the display of navigation links, user authentication status, and provides a logout button for authenticated users.

import { useContext } from "react";
import { Link, NavLink, useNavigate } from "react-router-dom";
import { AuthContext } from "./AuthProvider";

const Header = () => {
  // Access the user, logOut, and loading state from the AuthContext
  const { user, logOut, loading } = useContext(AuthContext);

  // Use the useNavigate hook to programmatically navigate between pages
  const navigate = useNavigate();

  // Handle user logout
  const handleSignOut = () => {
    logOut()
      .then(() => {
        console.log("User logged out successfully");
        navigate("/login"); // Redirect to the login page after logout
      })
      .catch((error) => console.error(error));
  };

  // Define navigation links based on user authentication status
  const navLinks = (
    <>
      <li>
        <NavLink to="/">Home</NavLink>
      </li>
      {!user && (
        <>
          <li>
            <NavLink to="/login">Login</NavLink>
          </li>
          <li>
            <NavLink to="/sign-up">Sign-Up</NavLink>
          </li>
        </>
      )}
    </>
  );

  // Render loading indicator if authentication state is still loading
  return loading ? (
    <span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
  ) : (
    <div>
      {/* Render the navigation bar */}
      <div className="navbar bg-base-100">
        <div className="navbar-start">
          {/* Dropdown menu for mobile devices */}
          <div className="dropdown">
            <label tabIndex={0} className="btn btn-ghost lg:hidden">
              {/* Hamburger icon */}
              <svg
                xmlns="http://www.w3.org/2000/svg"
                className="h-5 w-5"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                  d="M4 6h16M4 12h8m-8 6h16"
                />
              </svg>
            </label>
            {/* Dropdown content with navigation links */}
            <ul
              tabIndex={0}
              className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
            >
              {navLinks}
            </ul>
          </div>
          {/* Application title */}
          <a className="btn btn-ghost normal-case text-xl">Firebase Auth</a>
        </div>
        <div className="navbar-center hidden lg:flex">
          {/* Horizontal navigation menu for larger screens */}
          <ul className="menu menu-horizontal px-1">{navLinks}</ul>
        </div>
        <div className="navbar-end">
          {/* Display user information and logout button if authenticated */}
          {user && <a className="btn">{user.displayName}</a>}
          {user && (
            <div className="dropdown dropdown-end">
              <label tabIndex={0} className="btn btn-ghost btn-circle avatar">
                {/* User profile picture */}
                <div className="w-10 rounded-full">
                  <img src="/images/stock/photo-1534528741775-53994a69daeb.jpg" />
                </div>
              </label>
              {/* Dropdown content for user profile */}
              <ul
                tabIndex={0}
                className="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
              >
                <li>
                  <Link to="/profile">
                    {/* Profile link */}
                    <span className="justify-between">Profile</span>
                  </Link>
                </li>
                <li>
                  <a onClick={handleSignOut}>Logout</a>
                  {/* Logout button */}
                </li>
              </ul>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

export default Header;

Explanation:

  • The Header component uses the useContext hook to access the user, logOut, and loading state from the AuthContext. This allows it to display the user's information and handle authentication actions.

  • It uses the useNavigate hook from react-router-dom to navigate to different pages in the application.

  • The handleSignOut function is responsible for logging the user out. It calls the logOut function from the context and, upon success, redirects the user to the login page.

  • The navLinks variable defines the navigation links based on the user's authentication status. If the user is not authenticated, it includes links to the login and sign-up pages.

  • The component conditionally renders a loading indicator if the loading state is true.

  • The navigation bar is styled using CSS classes provided by the application's design framework (e.g., navbar, btn, menu, etc.).

Login Component

The Login component provides a form for users to log in to their accounts.

import { useContext } from "react";
import { AuthContext } from "./AuthProvider";
import { useNavigate } from "react-router-dom";

const Login = () => {
  const { loginUser, loading, user } = useContext(AuthContext);
  const navigate = useNavigate();

  // If authentication is still loading, display a loading indicator
  if (loading) {
    return (
      <span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
    );
  }

  // If the user is already authenticated, redirect to the home page
  if (user) {
    navigate("/");
  }

  // Handle form submission for user login
  const handleFormSubmit = (e) => {
    e.preventDefault();
    const email = e.target.email.value;
    const password = e.target.password.value;
    loginUser(email, password)
      .then((result) => {
        console.log(result);
        navigate("/");
      })
      .catch((error) => console.log(error.message));

    e.target.reset();
  };

  // Render the login form
  return (
    <div>
      <div className="min-h-screen bg-base-200">
        <div className="hero-content flex-col">
          <div className="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
            <div className="card-body">
              <form onSubmit={handleFormSubmit}>
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Email</span>
                  </label>
                  <input
                    type="text"
                    name="email"
                    placeholder="Email"
                    className="input input-bordered"
                  />
                </div>
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Password</span>
                  </label>
                  <input
                    type="password"


                    name="password"
                    placeholder="Password"
                    className="input input-bordered"
                  />
                </div>
                <div className="form-control mt-6">
                  <button className="btn btn-primary">Login</button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Login;

Explanation:

  • The Login component utilizes the useContext hook to access the loginUser, loading, and user state from the AuthContext.

  • It checks if authentication is still loading, and if so, displays a loading indicator.

  • If the user is already authenticated (user is not null), it redirects them to the home page.

  • The handleFormSubmit function is triggered when the user submits the login form. It retrieves the email and password from the form, calls the loginUser function from the context, and handles success and error scenarios.

  • The component renders a login form with email and password input fields.

SignUp Component

The SignUp component provides a form for users to create a new account.

import { useContext, useState } from "react";
import { AuthContext } from "./AuthProvider";
import { updateProfile } from "firebase/auth";
import { useNavigate } from "react-router-dom";

const SignUp = () => {
  const { createUser, user, loading } = useContext(AuthContext);
  const [selectedImage, setSelectedImage] = useState(null);
  const navigate = useNavigate();

  // If authentication is still loading, display a loading indicator
  if (loading) {
    return (
      <span className="loading loading-dots loading-lg flex item-center mx-auto"></span>
    );
  }

  // If the user is already authenticated, redirect to the home page
  if (user) {
    navigate("/");
  }

  // Handle form submission for user registration
  const handleFormSubmit = (e) => {
    e.preventDefault();
    const name = e.target.name.value;
    const email = e.target.email.value;
    const password = e.target.password.value;
    createUser(email, password)
      .then((result) => {
        // Update user profile with display name
        updateProfile(result.user, {
          displayName: name,
        });
        navigate("/");
        console.log(result);
      })
      .catch((error) => {
        console.log(error);
      });
    e.target.reset();
  };

  // Handle image upload (not shown in the code, but you can add it)

  // Render the sign-up form
  return (
    <div>
      <div className="min-h-screen bg-base-200">
        <div className="hero-content flex-col">
          <div className="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
            <div className="card-body">
              <form onSubmit={handleFormSubmit}>
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Name</span>
                  </label>
                  <input
                    type="text"
                    name="name"
                    placeholder="Name"
                    className="input input-bordered"
                  />
                </div>
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Email</span>
                  </label>
                  <input
                    type="email"
                    name="email"
                    placeholder="Email"
                    className="input input-bordered"
                  />
                </div>
                <div className="form-control">
                  <label className="label">
                    <span className="label-text">Password</span>
                  </label>
                  <input
                    type="password"
                    name="password"
                    placeholder="Password"
                    className="input input-bordered"
                  />
                </div>
                <div className="form-control mt-6">
                  <button className="btn btn-primary">Sign Up</button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default SignUp;

Explanation:

  • The SignUp component uses the useContext hook to access the createUser, user, and loading state from the AuthContext.

  • It checks if authentication is still loading, and if so, displays a loading indicator.

  • If the user is already authenticated (user is not null), it redirects them to the home page.

  • The handleFormSubmit function is triggered when the user submits the sign-up form. It retrieves the name, email, and password from the form, calls the createUser function from the context to create a new account, and updates the user's profile with the provided name.

  • The component renders a sign-up form with name, email, and password input fields. Note that the image upload part is not shown but can be added.

Profile Component

The Profile component displays the user's profile information.

import { useContext } from "react";
import { AuthContext } from "./AuthProvider";

const Profile = () => {
  const { user } = useContext(AuthContext);

  // Render user's profile information
  return (
    <div>
      <div className="hero min-h-screen bg-base-200">
        <div className="hero-content flex-col lg:flex-row">
          <img
            src="/images/stock/photo-1635805737707-575885ab0820.jpg"
            className="max-w-sm rounded-lg shadow-2xl"
          />
          <div>
            <h1 className="text-5xl font-bold">{user?.displayName}</h1>
            <p className="py-6">{user?.email}</p>
            <button className="btn btn-primary">Get Started</button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Profile;

Explanation:

  • The Profile component utilizes the useContext hook to access the user information from the AuthContext.

  • It renders the user's profile information, including their display name, email, and a "Get Started" button.

  • The user's profile picture can be displayed using the provided image source URL.

These components work together to provide a complete user authentication system in a React application using Firebase Authentication. The Header component handles navigation and user authentication status, while the Login, SignUp, and Profile components manage the respective functionalities for user authentication and profile management.

PrivateRoute Component

// PrivateRoute.js
import { useContext } from "react";
import { AuthContext } from "./AuthProvider";
import PropTypes from "prop-types";
import { Navigate } from "react-router-dom";

const PrivateRoute = ({ children }) => {
  const { loading, user } = useContext(AuthContext);

  if (loading) {
    return <span className="loading loading-dots loading-lg"></span>;
  }

  if (user) {
    return children;
  }

  return <Navigate to="/login" />;
};



PrivateRoute.propTypes = {
  children: PropTypes.node,
};

export default PrivateRoute;

Setting Up Routing

Lastly, let's set up routing using the react-router-dom library and wrap our app with the AuthProvider.

// Routes.js
import { createBrowserRouter } from "react-router-dom";
import App from "./App";
import Login from "./components/Login";
import SignUp from "./components/SignUp";
import Profile from "./components/Profile";
import PrivateRoute from "./AuthProvider/PrivateRoute";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      {
        path: "/login",
        element: <Login />,
      },
      {
        path: "/sign-up",
        element: <SignUp />,
      },
      {
        path: "/profile",
        element: (
          <PrivateRoute>
            <Profile />
          </PrivateRoute>
        ),
      },
    ],
  },
]);

export default router;

Conclusion

You've now created a Firebase Authentication system in a React app. You have authentication components for login, sign-up, and a user profile, as well as a PrivateRoute to protect routes that require authentication. Firebase provides a reliable and secure way to manage user authentication, making it a great choice for building user-centric applications.

Follow me on : Github Linkedin