Creating a MERN stack app that uses Firebase Authentication - Part Two

1/25/2022 · 8 minute read · 0 comments · 2333 views

Orginally posted on dev.to.

My favorite stack to use is the MERN stack. For those of you who aren’t sure what the acronym stands for its MongoDB, Express, React, and Node. These are frameworks and libraries that offers a powerful way to bootstrap a new application. Paired with Firebase it’s relatively simple to deliver a safe authentication system that you can use both on the back end and the front end of your application.

This article series will cover the following things:

  • Creating an Express server with a MongoDB database connected and using Firebase Admin SDK. Check out Part One.
  • Setting up a client side React App that uses Firebase for authentication.
  • If you just want take a look at the code and can divine more from that, check out the public repo I created.

React Front End

One important note, for the Front End I used Vite to bootstrap the application, but you could easily use Create React App as well.

client/src/main.jsx

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { StoreProvider } from "easy-peasy";
import store from "./stores/store";

ReactDOM.render(
  <React.StrictMode>
    <StoreProvider store={store}>
      <App />
    </StoreProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

This is the main entry point into our application. Everything here is pretty standard for React, but one important thing to note is we’re using a library called Easy Peasy. It essentially is a state management library and is very simple to setup, being a wrapper around Redux.

client/src/stores/store.js

import { createStore, action } from "easy-peasy";

const store = createStore({
  authorized: false,
  setAuthorized: action((state, payload) => {
    state.authorized = true;
  }),
  setUnauthorized: action((state, payload) => {
    state.authorized = false;
  })
});

export default store;

This is our setup for Easy Peasy. We’re tracking just a single state variable, but you could easily add more things to store here. Once we’re logged into Firebase or there’s a change in the auth state, we’ll be using the functions here to update and modify the boolean on if the user is authorized. If Easy Peasy isn’t your speed, you could easily replace this with Redux, Recoil, Mobx, the Context API, or any other state management solution.

client/src/services/firebase.js

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

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: ""
};

initializeApp(firebaseConfig);

const auth = getAuth();

export default {
  auth
};

Much like our Back End, we have to setup our Firebase service. The firebaseConfig is something you will get when you create a new project and add a web app to your project. I left it blank for a good reason as I didn’t want to share the information on my Firebase project. That being said, all you need to do is copy and paste your information from Firebase and you should be good to go.

client/src/App.jsx

import "./App.css";
import UnauthorizedRoutes from "./routes/UnauthorizedRoutes";
import AuthorizedRoutes from "./routes/AuthorizedRoutes";
import { useStoreState, useStoreActions } from "easy-peasy";
import firebaseService from "./services/firebase";
import { useEffect, useState } from "react";

function App() {
  const [loading, setLoading] = useState(true);
  const authorized = useStoreState((state) => state.authorized);
  const setAuthorized = useStoreActions((actions) => actions.setAuthorized);
  const setUnauthorized = useStoreActions((actions) => actions.setUnauthorized);

  const authStateListener = () => {
    firebaseService.auth.onAuthStateChanged(async (user) => {
      if (!user) {
        setLoading(false);
        return setUnauthorized();
      }

      setLoading(false);
      return setAuthorized();
    });
  };

  useEffect(() => {
    authStateListener();
  }, [authStateListener]);

  return (
    <div className="App" style={{ padding: 16 }}>
      {loading ? (
        <p>Loading...</p>
      ) : authorized ? (
        <AuthorizedRoutes />
      ) : (
        <UnauthorizedRoutes />
      )}
    </div>
  );
}

export default App;

In our App.jsx we tackle a few different things. First off we make sure we show a loading indication when the app first renders, because we’re essentially showing certain routes depending on we’re authenticated or not. The authStateListener function monitors through a useEffect the Firebase authentication state. If there’s a user, it sets the global state to true through Easy Peasy, otherwise it’s false.

client/src/routes/AuthorizedRoutes.jsx

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AuthorizedNav from "../components/navigation/AuthorizedNav";
import DashboardPage from "../components/pages/Dashboard";

export default function UnauthorizedRoutes() {
  return (
    <Router>
      <AuthorizedNav />
      <Routes>
        <Route path="/" element={<DashboardPage />} />
        <Route
          path="*"
          element={
            <main>
              <p>Not found.</p>
            </main>
          }
        />
      </Routes>
    </Router>
  );
}

If we are Authorized through Firebase Authentication, we have access to these routes. Right now it’s a single route with a dashboard page being rendered. One could easily add more routes that can only be seen while logged in, such as a Settings page, or anything that is relevant to the type of app it’s supposed to be.

client/src/routes/UnauthorizeRoutes.jsx

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import UnauthorizedNav from "../components/navigation/UnauthorizedNav";
import HomePage from "../components/pages/Home";
import SignInPage from "../components/pages/SignIn";
import SignUpPage from "../components/pages/SignUp";

export default function UnauthorizedRoutes() {
  return (
    <Router>
      <UnauthorizedNav />
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/signup" element={<SignUpPage />} />
        <Route path="/signin" element={<SignInPage />} />
        <Route
          path="*"
          element={
            <main>
              <p>Not found.</p>
            </main>
          }
        />
      </Routes>
    </Router>
  );
}

If we are logged out, we can only Sign Up, Sign In, or see our Homepage. Just like with our authorized routes, you could easily add more routes, things like a forgot password route, about page, contact page, and etc….

client/src/components/navigation/AuthorizedNav.jsx

import { Link } from "react-router-dom";
import firebaseService from "../../services/firebase";

export default function AuthorizedNav() {
  const logUserOut = async () => {
    await firebaseService.auth.signOut();
  };
  return (
    <nav>
      <ul style={{ listStyleType: "none", display: "flex" }}>
        <li style={{ marginRight: ".5rem" }}>
          <Link to="/">Dashboard</Link>
        </li>
        <li>
          <button
            style={{
              textDecoration: "underline",
              border: "none",
              backgroundColor: "inherit",
              fontSize: "1rem",
              padding: 0
            }}
            onClick={logUserOut}
          >
            Sign Out
          </button>
        </li>
      </ul>
    </nav>
  );
}

Our navigation reflects the routes we have while we are authenticated. However, our sign out performs and action through Firebase. This will trickle back up to our App.jsx and kick us out of any authorized routes.

client/src/components/navigation/UnauthorizedNav.jsx

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

export default function UnauthorizedNav() {
  return (
    <nav>
      <ul style={{ listStyleType: "none", display: "flex" }}>
        <li style={{ marginRight: ".5rem" }}>
          <Link to="/">Home</Link>
        </li>
        <li style={{ marginRight: ".5rem" }}>
          <Link to="/signup">Sign Up</Link>
        </li>
        <li>
          <Link to="/signin">Sign In</Link>
        </li>
      </ul>
    </nav>
  );
}

This is our navigation for the unauthorized routes. We can only visit the Sign Up, Sign In, or Home page.

client/src/components/pages/Home.jsx

export default function HomePage() {
  return <h1>Home</h1>;
}

Right now our Home page is a simple header, just to provide an example.

client/src/components/pages/SignIn.jsx

import { useStoreActions } from "easy-peasy";
import { signInWithEmailAndPassword } from "firebase/auth";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import firebaseService from "../../services/firebase";

export default function SignInPage() {
  const location = useLocation();
  const navigate = useNavigate();
  const [fields, setFields] = useState({
    email: "",
    password: ""
  });
  const [error, setError] = useState("");

  const setAuthorized = useStoreActions((actions) => actions.setAuthorized);

  const handleChange = (e) => {
    setFields({ ...fields, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const user = await signInWithEmailAndPassword(
        firebaseService.auth,
        fields.email,
        fields.password
      );
      if (user) {
        setAuthorized();
        navigate("/");
        console.log("Called");
      }
    } catch (err) {
      console.log(err);
      setError("Invalid email address or password.");
    }
  };

  return (
    <main>
      {location.state && location.state.message ? (
        <p style={{ color: "green" }}>{location.state.message}</p>
      ) : null}
      <h1>Sign In</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email Address</label>
        </div>
        <div>
          <input
            type="email"
            name="email"
            value={fields.email}
            onChange={handleChange}
            required
          />
        </div>
        <div style={{ marginTop: "1rem" }}>
          <label htmlFor="password">Password</label>
        </div>
        <div>
          <input
            type="password"
            name="password"
            value={fields.password}
            onChange={handleChange}
            required
          />
        </div>
        {error ? <p style={{ color: "red" }}>Error: {error}</p> : null}
        <div style={{ marginTop: "1rem" }}>
          <button type="submit">Sign In</button>
        </div>
      </form>
    </main>
  );
}

On the Sign In page we have a very simple form where we are collecting the user’s email and password. When they click the button to sign in, it then fires Firebase auth function that alters the state of whether or not we are authorized, and also returns the user. And then the function navigates us away from Sign In to the / route, which should take us to our Dashboard page.

client/src/components/pages/SignUp.jsx

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

export default function SignUpPage() {
  const [fields, setFields] = useState({
    email: "",
    name: "",
    password: "",
    confirmPassword: ""
  });
  const [error, setError] = useState("");

  const navigate = useNavigate();

  const handleChange = (e) => {
    setFields({ ...fields, [e.target.name]: e.target.value });
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (fields.password.length < 6) {
      return setError("Password must be at least 6 characters in length.");
    }
    if (fields.confirmPassword !== fields.password) {
      return setError("Password and confirm password must match.");
    }

    try {
      const req = await axios.post("http://localhost:4444/api/user", {
        email: fields.email,
        password: fields.password,
        name: fields.name
      });
      const message = req.data.success;
      return navigate("/signin", {
        replace: true,
        state: {
          message
        }
      });
    } catch (err) {
      const errMessage = err.response.data.error;
      return setError(errMessage);
    }
  };

  return (
    <div>
      <h1>Sign Up</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">Email Address</label>
        </div>
        <div>
          <input
            type="email"
            name="email"
            value={fields.email}
            onChange={handleChange}
            required
          />
        </div>
        <div style={{ marginTop: "1rem" }}>
          <label htmlFor="name">Name</label>
        </div>
        <div>
          <input
            type="text"
            name="name"
            value={fields.name}
            onChange={handleChange}
            required
          />
        </div>
        <div style={{ marginTop: "1rem" }}>
          <label htmlFor="password">Password</label>
        </div>
        <div>
          <input
            type="password"
            name="password"
            value={fields.password}
            onChange={handleChange}
            required
          />
        </div>
        <div style={{ marginTop: "1rem" }}>
          <label htmlFor="confirmPassword">Confirm Password</label>
        </div>
        <div>
          <input
            type="password"
            name="confirmPassword"
            value={fields.confirmPassword}
            onChange={handleChange}
            required
          />
        </div>

        {error ? <p style={{ color: "red" }}>Error: {error}</p> : null}
        <div style={{ marginTop: "1rem" }}>
          <button type="submit">Sign Up</button>
        </div>
      </form>
    </div>
  );
}

Our Sign Up page also contains a form that gathers information from the user. We are getting their email, their name, their password and confirming that password. After clicking Sign Up we use axios to make a post request to our API endpoint to add the new user. If there’s any errors, we handle those as well and display them on the screen for the user.

client/src/components/pages/Dashboard.jsx

import { useEffect, useState } from "react";
import firebaseService from "../../services/firebase";
import axios from "axios";

export default function DashboardPage() {
  const [loadingUser, setLoadingUser] = useState(true);
  const [user, setUser] = useState(null);

  const getUser = async () => {
    try {
      const token = await firebaseService.auth.currentUser.getIdToken(true);
      console.log(token);
      const req = await axios.get("http://localhost:4444/api/user", {
        headers: {
          authorization: `Bearer ${token}`
        }
      });
      console.log(req.data);
      if (req.data) {
        setUser(req.data);
        setLoadingUser(false);
      }
    } catch (err) {
      console.error(err);
    }
  };

  useEffect(() => {
    getUser();
  }, []);

  return (
    <>
      <h1>Dashboard</h1>
      {loadingUser ? (
        <p>Loading User</p>
      ) : (
        <div>
          <p>Name: {user.name}</p>
          <p>FirebaseID: {user.firebaseId}</p>
          <p>Email: {user.email}</p>
        </div>
      )}
    </>
  );
}

The final page we look at is our Dashboard, which if you remember can only be accessed while you are authorized and authenticated by Firebase. On this page we make a request to our api to get the user data and conditionally display it on the screen.

As you can see through these code examples, in a MERN stack application it’s not very difficult to integrate Firebase auth. We can use it on our Back End to protect our api routes and on our Front End to protect which pages and components we want to render to the user. We can pass our token along in the process each time we make HTTP requests. While it was out of scope for this guide, you could even integrate OAuth providers through Firebase as well, adding even more power to the arsenal. I hope these examples are useful to anyone trying to integrate with Firebase in their MERN stack application.