DEV Community

Cover image for Build Authentication Using Firebase | React | Express
Hana Belay
Hana Belay

Posted on

Build Authentication Using Firebase | React | Express

Hi everyone, in this tutorial, we are going to fire up an authentication system using React, Firebase, and Express. We will also use Tailwind to create the user interface.

Table of Contents

  1. Introduction
  2. React and Tailwind Set Up
    1. React Router
    2. Register and Login Components
  3. Firebase Set Up
  4. Firebase and React Integration
    1. React Context
    2. Error Handling
    3. Profile
    4. Logout
  5. Enable Dark Mode
  6. Navigation
  7. Private Route
  8. Express Server
    1. Express Boilerplate
    2. Enabling Cors
    3. Using Environment Variables
  9. Connect with React
    1. Sending Firebase Token to Express
    2. Creating the Middleware
  10. Final Thoughts

Introduction

To set up a registration/authentication system, we will leverage Firebase, a Backend-as-a-Service (Baas). What’s a BaaS?

Cloud computing has transformed the way we use technology. The BaaS cloud service model allows developers to focus on the frontend of an application by abstracting away all the stuff that goes behind it. Through BaaS, developers outsource pre-written software for different activities such as user authentication, database management, cloud storage, hosting, push notifications, and so on. We are going to use the user authentication service provided by Firebase for this project.

Here is the workflow:

  1. React will send a request to register and login a user to Firebase.
  2. Firebase will give a token to React
  3. React will use that token to send subsequent requests to an Express server.
  4. The Express server will verify the token using the Firebase admin SDK and sends a response back to React.

Quick Note:

  • Technically you don’t need Express for this if you aren’t going to use a backend for your project. However, in our case, we need an Express server to later create the chat API endpoints and connect with MongoDB.
  • Also, if you encounter any issues throughout the tutorial, you can check out the code in the GitHub repository.

React and Tailwind Set Up

Let's create a React application first. Type in the following to create a React app.

npx create-react-app frontend
Enter fullscreen mode Exit fullscreen mode

Remove git since we won’t use it inside the frontend app. If you are on Linux, you can remove it by running this:

cd frontend
rm -rf .git
Enter fullscreen mode Exit fullscreen mode

Alright, we are going to use Tailwind CSS to create the components, so let’s install it.

Make sure you are inside the frontend folder, open up your terminal and install Tailwind CSS


# Installs tailwindcss and its dependencies
npm install -D tailwindcss postcss autoprefixer

# Generates tailwind.config.js and postcss.config.js files
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Open tailwind.config.js and configure your template paths.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Open index.css, remove the content and add the following Tailwind directives. Check this out to learn more about these Tailwind-specific at-rules.

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Now, let’s delete files we won’t be using. The final structure should look something like this:

folder structure

Your index.js file should look like this:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

And finally to make sure everything is set up correctly, add the following to App.js and start the project.

// App.js

function App() {
  return (
    <div>
      <h1 className="text-3xl font-bold underline">Hello world!</h1>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Start the project:

npm run start
Enter fullscreen mode Exit fullscreen mode

hello world tailwind

Perfect! now inside the src folder, create another folder named components. Then, inside components, create a folder named accounts . We will put user auth-related components inside it.

Inside accounts, create 2 files named Register.js and Login.js

For now, put a boilerplate like the following. If you are on VSCode, you can generate this by using the rfc shortcut.

// Register.js

import React from "react";

export default function Register() {
  return <div>Register</div>;
}
Enter fullscreen mode Exit fullscreen mode
// Login.js

import React from "react";

export default function Login() {
  return <div>Login</div>;
}
Enter fullscreen mode Exit fullscreen mode

React Router

We will be using React router V6 for routing between different pages. Let’s go ahead and install it.

npm i react-router-dom
Enter fullscreen mode Exit fullscreen mode

Open App.js and create the path for login and register.

// App.js

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";

function App() {
  return (
    <Router>
      <Routes>
        <Route exact path="/register" element={<Register />} />
        <Route exact path="/login" element={<Login />} />
      </Routes>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Go to your browser and test the routes: http://localhost:3000/login and http://localhost:3000/register

Register and Login Components

Up until this point, we have been doing basic configuration. Let’s now get to creating the UI for account registration and login.

// Register.js

import { Link } from "react-router-dom";

export default function Register() {
  return (
    <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
            Register your account
          </h2>
        </div>
        <form className="mt-8 space-y-6">
          <div className="rounded-md shadow-sm -space-y-px">
            <div>
              <input
                id="email-address"
                name="email"
                type="email"
                autoComplete="email"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
                placeholder="Email address"
              />
            </div>
            <div>
              <input
                id="password"
                name="password"
                type="password"
                autoComplete="current-password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
                placeholder="Password"
              />
            </div>
            <div>
              <input
                id="confirmPassword"
                name="confirmPassword"
                type="password"
                autoComplete="current-password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
                placeholder="Password"
              />
            </div>
          </div>
          <div>
            <button
              type="submit"
              className=" w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-sky-800 hover:bg-sky-900"
            >
              Register
            </button>
          </div>
          <div className="flex items-center justify-between">
            <div className="text-sm">
              <Link
                to="/login"
                className="text-blue-600 hover:underline dark:text-blue-500"
              >
                Already have an account? Login
              </Link>
            </div>
          </div>
        </form>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • We have 3 fields. One for Email, the other two for password and password confirmation respectively. And for Login.js we have 2 fields; Email and password.
// Login.js

import { Link } from "react-router-dom";

export default function Login() {
  return (
    <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
            Login to your account
          </h2>
        </div>
        <form className="mt-8 space-y-6">
          <div className="rounded-md shadow-sm -space-y-px">
            <div>
              <input
                id="email-address"
                name="email"
                type="email"
                autoComplete="email"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
                placeholder="Email address"
              />
            </div>
            <div>
              <input
                id="password"
                name="password"
                type="password"
                autoComplete="current-password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
                placeholder="Password"
              />
            </div>
          </div>
          <div>
            <button
              type="submit"
              className=" w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-sky-800 hover:bg-sky-900"
            >
              Login
            </button>
          </div>
          <div className="flex items-center justify-between">
            <div className="text-sm">
              <Link
                to="/register"
                className="text-blue-600 hover:underline dark:text-blue-500"
              >
                Don't have an account? Register
              </Link>
            </div>
          </div>
        </form>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Don’t worry about the classes of the elements that are prefixed with dark. We will talk about them later when we enable dark mode.

Let’s now use the useState hook to store the form data. Inside Register.js, import useState at the top and add the following 3 states.

import { useState } from "react";

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
Enter fullscreen mode Exit fullscreen mode

Then, add an onChange event listener to the input fields as follows.

onChange={(e) => setEmail(e.target.value)}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => setConfirmPassword(e.target.value)}
Enter fullscreen mode Exit fullscreen mode

We now need a function that will handle the form submission.

async function handleFormSubmit(e) {
    e.preventDefault();

    // Here We will get form values and 
        // invoke a function that will register the user
}
Enter fullscreen mode Exit fullscreen mode

Add the function as an onSubmit event listener to the form.

onSubmit={handleFormSubmit}
Enter fullscreen mode Exit fullscreen mode

Do the same thing for Login.js. The only difference is that we don’t have a confirm password input field. The rest is the same. Now that our forms are ready, let’s move our attention to Firebase.

Firebase Set Up

If it’s your first time using Firebase, follow the following steps to create a project. Otherwise, you can move to the next section.

We first need to create a Firebase project using the Firebase console, so Log in and then click Create a project.

Firebase set up

Give any name you like for the project and click Continue. You will then be prompted to enable Google Analytics for your project (optional). We don’t need that.

Once you have completed the steps, you will be presented with a Dashboard. Click on Authentication from the navigation panel and go to the Sign-in Method tab. Enable Email/Password method since that’s what we are going to use.

Firebase and React Integration

For React to communicate with Firebase services, we need to install the firebase package.

npm i firebase
Enter fullscreen mode Exit fullscreen mode

After you have created a new project in Firebase, you need to create an application. Go to your Firebase Console dashboard, click on Project Settings, and select this icon (</>) to add Firebase to your web app. Doing so will give you config data.

Create a .env file inside the root directory of your project and store all these config data in it.

// .env

REACT_APP_FIREBASE_API_KEY = 
REACT_APP_FIREBASE_AUTH_DOMAIN = 
REACT_APP_FIREBASE_PROJECT_ID = 
REACT_APP_FIREBASE_STORAGE_BUCKET = 
REACT_APP_FIREBASE_MESSAGING_SENDER_ID = 
REACT_APP_FIREBASE_APP_ID = 
Enter fullscreen mode Exit fullscreen mode
  • When setting up environment variables, you should prefix them with REACT_APP_. In addition, make sure to put the .env file in your root folder. Finally, restart your server for the changes to take place.

Inside the src folder, create a folder called config and inside it, create a file called firebase.js This is where we will set up Firebase.

// firebase.js

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

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);

// gives us an auth instance
const auth = getAuth(app);

// in order to use this auth instance elsewhere
export default auth;
Enter fullscreen mode Exit fullscreen mode

Now that we have Firebase setup, we can start doing authentication.

React Context

We are going to use React context to set up the authentication because we want to access the current user everywhere in our app. If you aren’t familiar with React context, don’t worry. We are going to walk through it together.

React context allows us to share data (state) across our components more easily. This is great when you are passing data that can be used in any component of your application.

Inside the src folder, create a folder named contexts and inside it, create a file named AuthContext.js and put the following to set up context.

// AuthContext.js

import { createContext, useContext } from "react";

const AuthContext = createContext();

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const value = {};

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

What is going on? Well, let me explain

  • const AuthContext = createContext() will return a Consumer and a Provider component. Provider provides state to its children. It takes in the value prop and passes it down to its children components that might need to access them. The Consumer consumes and uses the state passed down to it by the Provider.
  • In the above case, the component that will wrap the Provider is AuthProvider We also exported it, so that we can use it elsewhere.
  • The custom useAuth hook will allow us to consume context in our application by returning a useContext instance of AuthContext

That is the basic setup for context. Let’s now add Firebase functions to register, login, and find the current user.

First, import createUserWithEmailAndPassword and signInWithEmailAndPassword that Firebase provides for user registration and login. Also, import auth from our earlier Firebase configuration.

// AuthContext.js

import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
} from "firebase/auth";

import auth from "../config/firebase";
Enter fullscreen mode Exit fullscreen mode

Users can create a new account by passing their email address and password. The functions return a Promise and we will consume that inside the register and login components.

function register(email, password) {
   // If the new account was created, the user is signed in automatically.
   return createUserWithEmailAndPassword(auth, email, password);
}

function login(email, password) {
   return signInWithEmailAndPassword(auth, email, password);
}
Enter fullscreen mode Exit fullscreen mode

Alright, but how can we get the signed-in user details? We can set an observer on the auth object. Before that, however, let’s first create a state that will store the current user and pass it down in the value props.

import { useState } from "react";
const [currentUser, setCurrentUser] = useState();

const value = {
    currentUser,
};
Enter fullscreen mode Exit fullscreen mode

We can now set the onAuthStateChanged observer on the auth object. This will notify us when the user is signed in and update the state.

import { useEffect } from "react";

const [loading, setLoading] = useState(true);

useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
    });

    return unsubscribe;
}, []);

return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
);
Enter fullscreen mode Exit fullscreen mode
  • The observer i.e. onAuthStateChanged is put inside a useEffect because we only want to render it once i.e. when the component mounts. When the component un-mounts, we unsubscribe from the event.
  • The loading state is initially set to true. When Firebase finishes its job and updates the user, we set it to false. This will prevent the children components from rendering while the action is taking place.

The final code for AuthContext.js should look like this:

// AuthContext.js

import { createContext, useContext, useState, useEffect } from "react";
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
} from "firebase/auth";

import auth from "../config/firebase";

const AuthContext = createContext();

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState();
  const [loading, setLoading] = useState(true);

  function register(email, password) {
    return createUserWithEmailAndPassword(auth, email, password);
  }

  function login(email, password) {
    return signInWithEmailAndPassword(auth, email, password);
  }

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user) => {
      setCurrentUser(user);
      setLoading(false);
    });

    return unsubscribe;
  }, []);

  const value = {
    currentUser,
    login,
    register,
  };

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let’s now wrap our components inside of the AuthProvider so that we have access to the context.

// App.js

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import { AuthProvider } from "./contexts/AuthContext"; // import this
import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";

function App() {
  return (
    <AuthProvider> // Wrap everything inside this provider
      <Router>
        <Routes>
          <Route exact path="/register" element={<Register />} />
          <Route exact path="/login" element={<Login />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, to use the register and currentUser context, head over to Register.js and import the useAuth hook.

// Register.js

import { useAuth } from "../../contexts/AuthContext";
Enter fullscreen mode Exit fullscreen mode

Then, import useEffect and useNavigate and add the following:

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

const navigate = useNavigate();
const { currentUser, register } = useAuth();
const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (currentUser) {
      navigate("/");
    }
  }, [currentUser, navigate]);
Enter fullscreen mode Exit fullscreen mode
  • const navigate = useNavigate(); React router hook that returns a function to navigate programmatically.
  • const { currentUser, register } = useAuth(); gets the currentUser and register contexts since those are what we will be using.
  • The loading state will be used to disable the button when the registration action is executing.
  • The useEffect hook is in place to prevent users from accessing the registration page while they are authenticated. It will redirect them to the home page if they try to do so.

Then inside the handleFormSubmit function we created earlier, we are going to handle the registration logic:

async function handleFormSubmit(e) {
    e.preventDefault();

    if (password !== confirmPassword) {
      return alert("Passwords do not match");
    }

    try {
      setLoading(true);
      await register(email, password);
      navigate("/profile");
    } catch (e) {
      alert("Failed to register");
    }

    setLoading(false);
  }
Enter fullscreen mode Exit fullscreen mode
  • First, if the passwords don’t match we exit out of the function by alerting the error. For now, we are not managing the errors. Later, however, we will handle them using a state.
  • The register function returns a promise, so we are using the await keyword to wait for the call to be settled. And we are using the try…catch block for error handling. If the call is successful, the user will be redirected to the profile page (we haven’t created it yet). However, the flow will be sent to the catch block if it fails.

To disable the submit button when the action is executing, add disabled={loading} to the submit Button.

Cool! let’s test what we have so far. Start your server and go to http://localhost:3000/register and fill in the form with passwords that don’t match. You should get the following alert:

firebase login

Now, fill in the correct credentials and you should be redirected to http://localhost:3000/profile. However, since we haven’t created this route yet, nothing will be displayed.

Do the same thing for the Login.js component. You only need to change the register call to login and remove the password confirmation check.

Error Handling

We want to use a single component that will handle and display errors (if there are any). For this, let’s use context.

Go to AuthContext.js and create this state:

const [error, setError] = useState("");
Enter fullscreen mode Exit fullscreen mode

Add it to the value props:

const value = {
    error,
    setError,
};
Enter fullscreen mode Exit fullscreen mode

Inside components, create a folder named layouts. And inside it, create a file called ErrorMessage.js We will use this component to display error messages from anywhere in our app.

First, let’s install herocions to use the X icon in our alert.

npm i @heroicons/react@v1
Enter fullscreen mode Exit fullscreen mode

Then inside ErrorMessage.js:

// ErrorMessage.js

import { XCircleIcon } from "@heroicons/react/solid";

import { useAuth } from "../../contexts/AuthContext";

export default function ErrorMessage() {
  const { error, setError } = useAuth();

  return (
    error && (
      <div className="flex justify-center">
        <div className="rounded-md max-w-md w-full bg-red-50 p-4 mt-4">
          <div className="flex">
            <div className="flex-shrink-0">
              <XCircleIcon
                onClick={() => setError("")}
                className="h-5 w-5 text-red-400"
                aria-hidden="true"
              />
            </div>
            <div className="ml-3">
              <h3 className="text-sm font-medium text-red-800">
                Error: {error}
              </h3>
            </div>
          </div>
        </div>
      </div>
    )
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The above snippet checks if there is an error and displays it. When the X icon is clicked it will set the error to an empty string.

Then render this component inside App.js at the top.

import ErrorMessage from "./components/layouts/ErrorMessage"; // add this import

<AuthProvider>
      <Router>
        <ErrorMessage /> // add this
        <Routes>
          <Route exact path="/register" element={<Register />} />
          <Route exact path="/login" element={<Login />} />
        </Routes>
      </Router>
    </AuthProvider>
Enter fullscreen mode Exit fullscreen mode

Now, let’s update Register.js to set the errors accordingly.

const { currentUser, register, setError } = useAuth(); // Get setError as well
Enter fullscreen mode Exit fullscreen mode
async function handleFormSubmit(e) {
    e.preventDefault();

    if (password !== confirmPassword) {
      return setError("Passwords do not match"); // Replace the alert with this
    }

    try {
      setError(""); // Remove error when trying to register
      setLoading(true);
      await register(email, password);
      navigate("/profile");
    } catch (e) {
      setError("Failed to register"); // Replace the alert with this
    }

    setLoading(false);
  }
Enter fullscreen mode Exit fullscreen mode

Do the same thing for Login.js and test it. Before testing it though, make sure to comment out the useEffect hook that checks if there is a current user so that it won’t redirect you to the home page.

You should get something like this:

Error handling

Profile

Now that we have registration and login set up, let’s create a route to set a name and avatar for users.

The Firebase user object has basic properties such as displayName, photoURL, phoneNumber, etc. You can check this by logging the currentUser object on the console. We are going to update the user’s profile by using the updateProfile function from Firebase.

Open AuthContext.js and import this function:

import { updateProfile } from "firebase/auth";
Enter fullscreen mode Exit fullscreen mode

Add the following function alongside register and login

function updateUserProfile(user, profile) {
    return updateProfile(user, profile);
}
Enter fullscreen mode Exit fullscreen mode
  • The updateProfile function takes a user and any user information we need to update.

Add updateUserProfile in value props.

const value = {
    // ...
    updateUserProfile,
  };
Enter fullscreen mode Exit fullscreen mode

We are going to update the display name and photo of the user. To generate cool avatars, we are going to use this API.

You can replace :sprites with malefemalehumanidenticoninitialsbottts,  avataaarsjdenticongridyor micah. The value of :seed can be anything you like but don't use any sensitive or personal data here!

Inside the src folder, create a folder called utils. And inside it, create a file called GenerateAvatar.js

// GenerateAvatar.js

const generateDiceBearAvataaars = (seed) =>
  `https://avatars.dicebear.com/api/avataaars/${seed}.svg`;

const generateDiceBearBottts = (seed) =>
  `https://avatars.dicebear.com/api/bottts/${seed}.svg`;

const generateDiceBearGridy = (seed) =>
  `https://avatars.dicebear.com/api/gridy/${seed}.svg`;

export const generateAvatar = () => {
  const data = [];

  for (let i = 0; i < 2; i++) {
    const res = generateDiceBearAvataaars(Math.random());
    data.push(res);
  }
  for (let i = 0; i < 2; i++) {
    const res = generateDiceBearBottts(Math.random());
    data.push(res);
  }
  for (let i = 0; i < 2; i++) {
    const res = generateDiceBearGridy(Math.random());
    data.push(res);
  }
  return data;
};
Enter fullscreen mode Exit fullscreen mode
  • The above function generates 6 random avatars and returns the array. We have exported the function so that we can use it elsewhere.

Now, create a file called Profile.js inside accounts. Most of the things in this file are going to be similar to Register.js and Login.js.

// Profile.js

import { generateAvatar } from "../../utils/GenerateAvatar";

const [avatars, setAvatars] = useState([]);

useEffect(() => {
    const fetchData = () => {
      const res = generateAvatar();
      setAvatars(res);
    };

    fetchData();
  }, []);
Enter fullscreen mode Exit fullscreen mode
  • This will generate avatars using the utility function we created earlier and set them to the avatars state when the component mounts.

We will also have the following state to track the selected avatar.

const [selectedAvatar, setSelectedAvatar] = useState();
Enter fullscreen mode Exit fullscreen mode

This is what Profile.js looks like now

// Profile.js

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

import { useAuth } from "../../contexts/AuthContext";
import { generateAvatar } from "../../utils/GenerateAvatar";

function classNames(...classes) {
  return classes.filter(Boolean).join(" ");
}

export default function Profile() {
  const navigate = useNavigate();

  const [username, setUsername] = useState("");
  const [avatars, setAvatars] = useState([]);
  const [selectedAvatar, setSelectedAvatar] = useState();
  const [loading, setLoading] = useState(false);

  const { currentUser, updateUserProfile, setError } = useAuth();

  useEffect(() => {
    const fetchData = () => {
      const res = generateAvatar();
      setAvatars(res);
    };

    fetchData();
  }, []);

  const handleFormSubmit = async (e) => {
    e.preventDefault();

    if (selectedAvatar === undefined) {
      return setError("Please select an avatar");
    }

    try {
      setError("");
      setLoading(true);
      const user = currentUser;
      const profile = {
        displayName: username,
        photoURL: avatars[selectedAvatar],
      };
      await updateUserProfile(user, profile);
      navigate("/");
    } catch (e) {
      setError("Failed to update profile");
    }

    setLoading(false);
  };

  return (
    <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div className="text-center">
          <h2 className="mt-4 text-3xl text-center tracking-tight font-light dark:text-white">
            Pick an avatar
          </h2>
        </div>
        <form className="space-y-6" onSubmit={handleFormSubmit}>
          <div className="flex flex-wrap -m-1 md:-m-2">
            {avatars.map((avatar, index) => (
              <div key={index} className="flex flex-wrap w-1/3">
                <div className="w-full p-1 md:p-2">
                  <img
                    alt="gallery"
                    className={classNames(
                      index === selectedAvatar
                        ? "border-4  border-blue-700 dark:border-blue-700"
                        : "cursor-pointer hover:border-4 hover:border-blue-700",
                      "block object-cover object-center w-36 h-36 rounded-full"
                    )}
                    src={avatar}
                    onClick={() => setSelectedAvatar(index)}
                  />
                </div>
              </div>
            ))}
          </div>

          <div className="rounded-md shadow-sm -space-y-px">
            <input
              id="username"
              name="username"
              type="text"
              autoComplete="username"
              required
              className="appearance-none rounded-none relative block w-full px-3 py-2 placeholder-gray-500 rounded-t-md bg-gray-50 border border-gray-300 text-gray-900 text-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 focus:z-10 sm:text-sm"
              placeholder="Enter a Display Name"
              defaultValue={currentUser.displayName && currentUser.displayName}
              onChange={(e) => setUsername(e.target.value)}
            />
          </div>
          <div>
            <button
              type="submit"
              disabled={loading}
              className="w-full py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
            >
              Update Profile
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, let’s create the route in App.js

import Profile from "./components/accounts/Profile"; // Add this to the imports list

<Route exact path="/profile" element={<Profile />} /> // Add this route
Enter fullscreen mode Exit fullscreen mode

Finally, go to http://localhost:3000/profile and check if everything is working. You will get something like this.

Profile page

When you update the profile, it will redirect you to a blank page, don’t worry we haven’t created a home page yet.

Logout

First, let’s add the Firebase function that will allow us to log out users. Inside your AuthContext.js, add the following

import { signOut } from "firebase/auth"; // Add this import

function logout() {
    return signOut(auth);
}
Enter fullscreen mode Exit fullscreen mode

Simple as that!

Then create a file called Logout.js inside the accounts folder. We are going to create a modal that will ask for the user’s confirmation to log out and fires the logout function. We are going to use the Dialog and Transition components from Headless UI so let’s install it first

npm i @headlessui/react
Enter fullscreen mode Exit fullscreen mode
// Logout.js

import { Fragment, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationIcon } from "@heroicons/react/outline";

import { useAuth } from "../../contexts/AuthContext";

export default function Logout({ modal, setModal }) {
  const cancelButtonRef = useRef(null);
  const navigate = useNavigate();

  const { logout, setError } = useAuth();

  async function handleLogout() {
    try {
      setError("");
      await logout();
      setModal(false);
      navigate("/login");
    } catch {
      setError("Failed to logout");
    }
  }

  return (
    <Transition.Root show={modal} as={Fragment}>
      <Dialog
        as="div"
        className="fixed z-10 inset-0 overflow-y-auto"
        initialFocus={cancelButtonRef}
        onClose={setModal}
      >
        <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0"
            enterTo="opacity-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100"
            leaveTo="opacity-0"
          >
            <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
          </Transition.Child>

          {/* This element is to trick the browser into centering the modal contents. */}
          <span
            className="hidden sm:inline-block sm:align-middle sm:h-screen"
            aria-hidden="true"
          >
            &#8203;
          </span>
          <Transition.Child
            as={Fragment}
            enter="ease-out duration-300"
            enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
            enterTo="opacity-100 translate-y-0 sm:scale-100"
            leave="ease-in duration-200"
            leaveFrom="opacity-100 translate-y-0 sm:scale-100"
            leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
          >
            <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
              <div className="bg-white dark:bg-gray-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                <div className="sm:flex sm:items-start">
                  <div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-gray-200 sm:mx-0 sm:h-10 sm:w-10">
                    <ExclamationIcon
                      className="h-6 w-6 text-red-600"
                      aria-hidden="true"
                    />
                  </div>
                  <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
                    <Dialog.Title
                      as="h3"
                      className="text-lg leading-6 font-medium text-gray-500 dark:text-gray-400"
                    >
                      Logging out
                    </Dialog.Title>
                    <div className="mt-2">
                      <p className="text-sm text-gray-500 dark:text-gray-400">
                        Are you sure you want to log out ?
                      </p>
                    </div>
                  </div>
                </div>
              </div>
              <div className="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                <button
                  type="button"
                  className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
                  onClick={handleLogout}
                >
                  Logout
                </button>
                <button
                  type="button"
                  className="mt-3 w-full inline-flex justify-center shadow-sm px-4 py-2  sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm
                  rounded-md border border-gray-300 bg-white text-gray-500 text-base font-medium hover:bg-gray-100 focus:outline-none focus:ring-gray-200 focus:ring-2 focus:ring-offset-2 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600
                  "
                  onClick={() => setModal(false)}
                  ref={cancelButtonRef}
                >
                  Cancel
                </button>
              </div>
            </div>
          </Transition.Child>
        </div>
      </Dialog>
    </Transition.Root>
  );
}
Enter fullscreen mode Exit fullscreen mode

Done!

Enable Dark Mode

If you have been paying attention to the Tailwind classes of our component elements, you must have seen something like dark:text-white This kind of style pattern is applied to the elements when dark mode is enabled. We haven’t enabled it yet, so let’s do that now.

Open your tailwind.config.js file and add the following:

darkMode: "class",
Enter fullscreen mode Exit fullscreen mode

Inside layouts, create a file called ThemeToggler.js and add a button that will toggle between light and dark mode.

// ThemeToggler.js

export default function ThemeToggler() {
  return (
    <button
      ref={themeToggleBtn}
      type="button"
      className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
      onClick={() => handleThemeToggle()}
    >
      <svg
        ref={themeToggleDarkIcon}
        className="hidden w-8 h-8"
        fill="currentColor"
        viewBox="0 0 20 20"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
      </svg>
      <svg
        ref={themeToggleLightIcon}
        className="hidden w-8 h-8"
        fill="currentColor"
        viewBox="0 0 20 20"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
          fillRule="evenodd"
          clipRule="evenodd"
        ></path>
      </svg>
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create a useRef hook to access the above theme toggle button, theme toggle light icon, and theme toggle dark icon.

import { useRef } from "react";

const themeToggleBtn = useRef();
const themeToggleLightIcon = useRef();
const themeToggleDarkIcon = useRef();
Enter fullscreen mode Exit fullscreen mode

Add refs to the toggle button, dark icon, and light icon SVGs respectively like this:

ref={themeToggleBtn}
ref={themeToggleDarkIcon}
ref={themeToggleLightIcon}
Enter fullscreen mode Exit fullscreen mode

For the button, add an onClick event listener:

onClick={() => handleThemeToggle()}
Enter fullscreen mode Exit fullscreen mode

Let’s now create this function. When the button is pressed it will check whether or not the user has a color-theme set in local storage previously, add/remove the dark class, and update local storage.

const handleThemeToggle = () => {
    themeToggleDarkIcon.current.classList.toggle("hidden");
    themeToggleLightIcon.current.classList.toggle("hidden");

    if (localStorage.getItem("color-theme")) {
      if (localStorage.getItem("color-theme") === "light") {
        document.documentElement.classList.add("dark");
        localStorage.setItem("color-theme", "dark");
      } else {
        document.documentElement.classList.remove("dark");
        localStorage.setItem("color-theme", "light");
      }
    } else {
      if (document.documentElement.classList.contains("dark")) {
        document.documentElement.classList.remove("dark");
        localStorage.setItem("color-theme", "light");
      } else {
        document.documentElement.classList.add("dark");
        localStorage.setItem("color-theme", "dark");
      }
    }
  };
Enter fullscreen mode Exit fullscreen mode

Next, when the component renders, we want to either set the dark or light mode by checking previous user preference or settings.

useEffect(() => {
    if (
      localStorage.getItem("color-theme") === "dark" ||
      (!("color-theme" in localStorage) &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      document.documentElement.classList.add("dark");
      themeToggleLightIcon.current.classList.remove("hidden");
    } else {
      document.documentElement.classList.remove("dark");
      themeToggleDarkIcon.current.classList.remove("hidden");
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

Final code for ThemeToggler.js

// ThemeToggler.js

import { useEffect, useRef } from "react";

export default function ThemeToggler() {
  const themeToggleBtn = useRef();
  const themeToggleLightIcon = useRef();
  const themeToggleDarkIcon = useRef();

  useEffect(() => {
    if (
      localStorage.getItem("color-theme") === "dark" ||
      (!("color-theme" in localStorage) &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      document.documentElement.classList.add("dark");
      themeToggleLightIcon.current.classList.remove("hidden");
    } else {
      document.documentElement.classList.remove("dark");
      themeToggleDarkIcon.current.classList.remove("hidden");
    }
  }, []);

  const handleThemeToggle = () => {
    themeToggleDarkIcon.current.classList.toggle("hidden");
    themeToggleLightIcon.current.classList.toggle("hidden");

    if (localStorage.getItem("color-theme")) {
      if (localStorage.getItem("color-theme") === "light") {
        document.documentElement.classList.add("dark");
        localStorage.setItem("color-theme", "dark");
      } else {
        document.documentElement.classList.remove("dark");
        localStorage.setItem("color-theme", "light");
      }
    } else {
      if (document.documentElement.classList.contains("dark")) {
        document.documentElement.classList.remove("dark");
        localStorage.setItem("color-theme", "light");
      } else {
        document.documentElement.classList.add("dark");
        localStorage.setItem("color-theme", "dark");
      }
    }
  };

  return (
    <button
      ref={themeToggleBtn}
      type="button"
      className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
      onClick={() => handleThemeToggle()}
    >
      <svg
        ref={themeToggleDarkIcon}
        className="hidden w-8 h-8"
        fill="currentColor"
        viewBox="0 0 20 20"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
      </svg>
      <svg
        ref={themeToggleLightIcon}
        className="hidden w-8 h-8"
        fill="currentColor"
        viewBox="0 0 20 20"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
          fillRule="evenodd"
          clipRule="evenodd"
        ></path>
      </svg>
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Finally, to apply dark mode to the body of our application, add the following in index.css. It will add our own default base styles for the body.

@layer base {
    body {
      @apply bg-white dark:bg-gray-900;
    }
  }
Enter fullscreen mode Exit fullscreen mode

Let’s quickly add a navigation component and we will test the logout and dark mode functionalities.

Navigation

The navigation will contain theme toggler, logout and profile icons.

Inside layouts create a file called Header.js

// Header.js

import { LogoutIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { Link } from "react-router-dom";

import { useAuth } from "../../contexts/AuthContext";
import Logout from "../accounts/Logout";
import ThemeToggler from "./ThemeToggler";

export default function Header() {
  const [modal, setModal] = useState(false);

  const { currentUser } = useAuth();

  return (
    <>
      <nav className="px- px-2 sm:px-4 py-2.5 bg-gray-50 border-gray-200 dark:bg-gray-800 dark:border-gray-700 text-gray-900 text-sm rounded border dark:text-white">
        <div className="container mx-auto flex flex-wrap items-center justify-between">
          <Link to="/" className="flex">
            <span className="self-center text-lg font-semibold whitespace-nowrap text-gray-900 dark:text-white">
              Chat App
            </span>
          </Link>
          <div className="flex md:order-2">
            <ThemeToggler />

            {currentUser && (
              <>
                <button
                  className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
                  onClick={() => setModal(true)}
                >
                  <LogoutIcon className="h-8 w-8" aria-hidden="true" />
                </button>

                <Link
                  to="/profile"
                  className="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full text-sm p-2.5"
                >
                  <img
                    className="h-8 w-8 rounded-full"
                    src={currentUser.photoURL}
                    alt=""
                  />
                </Link>
              </>
            )}
          </div>
        </div>
      </nav>
      {modal && <Logout modal={modal} setModal={setModal} />}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • We used conditional rendering to display the logout and profile icons only when the user is authenticated.
  • The modal state will be used to render the Logout modal for confirmation.

Finally, let’s render Header.js inside App.js

// App.js

import Header from "./components/layouts/Header"; // Add this

<AuthProvider>
    <Router>
      <Header /> // And this
      <ErrorMessage />
      <Routes>
        <Route exact path="/register" element={<Register />} />
        <Route exact path="/login" element={<Login />} />
        <Route exact path="/profile" element={<Profile />} />
      </Routes>
    </Router>
</AuthProvider>
Enter fullscreen mode Exit fullscreen mode

To test this go to http://localhost:3000/profile. You will get something like this when dark mode is enabled.

Dark mode Profile page

The logout modal (with light mode):

Logout modal

Great! moving on to private routes.

Private Route

Routes such as /profile should only be accessed if the user is logged in. To make such components private, let’s go ahead and create a higher-order component that will wrap them.

Inside the utils folder, create a file called WithPrivateRoute.js

// WithPrivateRoute.js

import { Navigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";

const WithPrivateRoute = ({ children }) => {
  const { currentUser } = useAuth();

    // If there is a current user it will render the passed down component
  if (currentUser) {
    return children;
  }

  // Otherwise redirect to the login route
  return <Navigate to="/login" />;
};

export default WithPrivateRoute;
Enter fullscreen mode Exit fullscreen mode

We can now use this higher-order component to wrap Profile.js

The final look of App.js:


// App.js

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

import { AuthProvider } from "./contexts/AuthContext";
import Register from "./components/accounts/Register";
import Login from "./components/accounts/Login";
import Profile from "./components/accounts/Profile";
import WithPrivateRoute from "./utils/WithPrivateRoute";
import Header from "./components/layouts/Header";
import ErrorMessage from "./components/layouts/ErrorMessage";

function App() {
  return (
    <AuthProvider>
      <Router>
        <Header />
        <ErrorMessage />
        <Routes>
          <Route exact path="/register" element={<Register />} />
          <Route exact path="/login" element={<Login />} />
          <Route
            exact
            path="/profile"
            element={
              <WithPrivateRoute>
                <Profile />
              </WithPrivateRoute>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

You can test this by trying to access the profile page without logging in.

This marks the end of the frontend section for authentication. Let’s now get started with Express to verify users for subsequent requests.


Express Server

Express Boilerplate

The backend is going to be an independent app, so in a new terminal type in the following:

npm init -y
Enter fullscreen mode Exit fullscreen mode

This will create a package.json file

Express package.json

Open it up and let’s modify a couple of things.

First, we are going to use ES 6 modules (import syntax), so let’s enable it.

"type": "module",
Enter fullscreen mode Exit fullscreen mode

We are also going to use nodemon so that we don’t need to restart our server every time we make changes. If you haven’t already, install it globally:

npm install -g nodemon
Enter fullscreen mode Exit fullscreen mode

Then add the following to the scripts (we will create server/index.js next)

"start": "nodemon server/index.js"
Enter fullscreen mode Exit fullscreen mode

Your package.json should look like this

{
  "name": "chat-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon server/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now install Express:

npm i express
Enter fullscreen mode Exit fullscreen mode

Create a folder named server, cd into it, and create index.js file. This is where we will set up our server.

mkdir server
cd server
touch index.js
Enter fullscreen mode Exit fullscreen mode

Inside index.js let’s add a simple endpoint to fire up and test our server.

// index.js

import express from "express";

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.get("/", (req, res) => {
  res.send("working fine");
});

const PORT = 3001;

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

To test it:

cd ../
npm start
Enter fullscreen mode Exit fullscreen mode

Now, if you go to http://localhost:3001/ on your browser, you should see the “working fine” output.

Enabling Cors

We need to enable cors to allow React to send requests to Express.

Install cors:

npm i cors
Enter fullscreen mode Exit fullscreen mode

And inside index.js

import cors from "cors"; // Add this to the list of imports

app.use(cors()); // Use the cors middleware
Enter fullscreen mode Exit fullscreen mode

Using Environment Variables

To use environment variables, let’s install dotenv

npm i dotnev
Enter fullscreen mode Exit fullscreen mode

Then create a .env file at the root of your project

touch .env
Enter fullscreen mode Exit fullscreen mode

Open it and add the port

PORT=3001
Enter fullscreen mode Exit fullscreen mode

Then in index.js import dotenv and access the PORT variable we just created.

import dotenv from "dotenv"; // Add to import list

dotenv.config(); // Configure dotenv to access the env variables

const PORT = process.env.PORT || 8080; // Use this instead of hardcoding it like before
Enter fullscreen mode Exit fullscreen mode

Connect with React

Now that we have enabled cors, let’s test this by sending a request from React.

Go to the frontend folder and open up Header.js from components/layouts. Add the following useEffect hook. Note that this is just for testing purposes. You can get rid of it later.

useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch("http://localhost:3001");
        console.log(await res.text());
      } catch (e) {
        console.log(e);
      }
    };

    fetchData();
  }, []);
Enter fullscreen mode Exit fullscreen mode
  • We are making a request to this http://localhost:3001 endpoint using the built-in fetch API and logging the response to the console. If you head over to your React app at http://localhost:3000/, you should see the “working fine” output in the console.

Dope! React can communicate with the backend now. However, what we want to do is prevent React from getting a response if a verified user token is not sent. We will do 2 things:

  1. Send the Firebase token from the frontend to the backend
  2. Create a middleware in Express to verify the token

Sending Firebase Token to Express

Again, open Header.js, and let’s modify the previous request to send the authentication token as a header Authorization: Bearer token. How can we get the token? Firebase makes this really easy.

// Header.js

import auth from "../../config/firebase";

useEffect(() => {
    const fetchData = async () => {
      try {
        const user = auth.currentUser;
        const token = user && (await user.getIdToken());

        const payloadHeader = {
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${token}`,
          },
        };
        const res = await fetch("http://localhost:3001", payloadHeader);
        console.log(await res.text());
      } catch (e) {
        console.log(e);
      }
    };

    fetchData();
}, []);
Enter fullscreen mode Exit fullscreen mode
  • First, we imported auth from our firebase config. We can use this to get the current user instance. getIdToken() returns the token if it has not expired or if it will not expire in the next five minutes. Otherwise, this will refresh the token and return a new one.

Now if you go to http://localhost:3000/ network tab, you can see the Authorization header being sent as well.

This is looking good. The next step is to verify the token in our backend.

Creating the Middleware

The middleware we are going to create will access the request to find the token. Then, we will verify it. How? Again, Firebase to the rescue! Firebase has a backend module that allows us to verify the token. Let’s install it.

Inside your root directory (where package.json and frontend folder are located):

npm i firebase-admin
Enter fullscreen mode Exit fullscreen mode

According to the docs to use Firebase-admin SDK we need:

  • [x] A Firebase project.
  • [x] A Firebase Admin SDK service account to communicate with Firebase. This service account is created automatically when you create a Firebase project or add Firebase to a Google Cloud project.
  • [ ] A configuration file with your service account's credentials.

We have done the first 2. To get the service account go to your Firebase Dashboard and open Settings in the side panel. Then click the Service Accounts tab and click Generate New Private Key, then confirm by clicking Generate Key.

Once you download the JSON file containing the keys, go back to your project and navigate to the server folder. Create a folder called config and inside it, create a file called serviceAccountKey.json. This is where we will put the generated keys.

cd server
mkdir config
cd config
touch serviceAccountKey.json
Enter fullscreen mode Exit fullscreen mode

After copy-pasting the keys into this file, create another file inside this config folder called firebase-config.js

touch firebase-config.js
Enter fullscreen mode Exit fullscreen mode

Then open this file and,

//firebase-config.js

import { initializeApp, cert } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

import serviceAccountKey from "./serviceAccountKey.json" assert { type: "json" };

const app = initializeApp({
  credential: cert(serviceAccountKey),
});

const auth = getAuth(app);
export default auth;
Enter fullscreen mode Exit fullscreen mode
  • We imported the JSON file with the Firebase configurations and used it to initialize the Firebase SDK. We then exported auth to use it elsewhere.

Next, inside the server folder, create a folder called middleware. Inside middleware, create a file called VerifyToken.js

Folder structure

Then, open VerifyToken.js and add the following:

// VerifyToken.js

import auth from "../config/firebase-config.js";

export const VerifyToken = async (req, res, next) => {
  const token = req.headers.authorization.split(" ")[1];

  try {
    const decodeValue = await auth.verifyIdToken(token);
    if (decodeValue) {
      req.user = decodeValue;
      return next();
    }
  } catch (e) {
    return res.json({ message: "Internal Error" });
  }
};
Enter fullscreen mode Exit fullscreen mode
  • First, we imported the Firebase auth we created earlier.
  • const token = req.headers.authorization.split(" ")[1]; Here we are splitting the authorization header to get the token from the request.
  • auth.verifyIdToken(token); will decode the token to its associated user. Finally, we check if this decoded value is the same as the user sending the request.

Let’s now use this middleware in index.js


import { VerifyToken } from "./middlewares/VerifyToken.js";

app.use(VerifyToken); // Add this middleware
Enter fullscreen mode Exit fullscreen mode

index.js will look like this:

// index.js

import express from "express";
import cors from "cors";
import dotenv from "dotenv";

import { VerifyToken } from "./middlewares/VerifyToken.js";

const app = express();

dotenv.config();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.use(VerifyToken);

const PORT = process.env.PORT || 8080;

app.get("/", (req, res) => {
  res.send("working fine");
});

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Now, if you go back to http://localhost:3000/ and refresh, you will see “working fine” outputted on the console. However, if you replace the token with some random value or remove the header, Express will reject the request.

Perfect! Everything is working now.


Final Thoughts

Got lost somewhere throughout the process? No problem, check out the project on GitHub or ask your question in the comments section.

Feedback or comment? let me know down below. Thanks!

Top comments (1)

Collapse
 
huhu1030 profile image
Eryoruk Huseyin • Edited

Very usefull article.
In the repo, ChatServices has a method :
const getUserToken = async () => {
const user = auth.currentUser;
const token = user && (await user.getIdToken());
return token;
};

Wouldn't be better if this was outside of that class thus other future service could use it ? Any suggestion ?