Loading...

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 backend and the frontend 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
  • 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.

Express Backend

src/server.mjs


import express from "express";
import cors from "cors";
import config from "./config/index.mjs";
import db from "./config/db.mjs";
import userRouter from "./api/user.mjs";

const app = express();

db(config.MONGO_URI, app);

app.use(cors({ origin: true }));
app.use(express.json());
app.use("/api/user", userRouter);

app.listen(config.PORT, () =>
  console.log(`App listening on PORT ${config.PORT}`)
);

We start by importing all of our dependencies to get the server setup. Initialize that app and call our database function to connect to MongoDB. Then we connect the middleware we’re going to be using and begin listening on our PORT, a pretty standard Express app setup.

src/config/index.mjs


import dotenv from "dotenv";

dotenv.config();

export default {
  PORT: process.env.PORT,
  MONGO_URI: process.env.MONGO_URI,
  FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
  FIREBASE_PRIVATE_KEY_ID: process.env.FIREBASE_PRIVATE_KEY_ID,
  FIREBASE_PRIVATE_KEY:
    process.env.FIREBASE_PRIVATE_KEY &&
    process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
  FIREBASE_CLIENT_EMAIL: process.env.FIREBASE_CLIENT_EMAIL,
  FIREBASE_CLIENT_ID: process.env.FIREBASE_CLIENT_ID,
  FIREBASE_AUTH_URI: process.env.FIREBASE_AUTH_URI,
  FIREBASE_TOKEN_URI: process.env.FIREBASE_TOKEN_URI,
  FIREBASE_AUTH_CERT_URL: process.env.FIREBASE_AUTH_CERT_URL,
  FIREBASE_CLIENT_CERT_URL: process.env.FIREBASE_CLIENT_CERT_URL
};


We use dotenv to pull in our environmental variables, of which includes our port, our MongoDB URI, and all of Firebase certificate information we need to use the Firebase Admin SDK.

src/config/db.mjs


import { MongoClient } from "mongodb";

export default async function (connectionString, app) {
  const client = new MongoClient(connectionString);
  try {
    await client.connect();
    app.locals.db = client.db("mern-firebase");
    console.log("+++ Database connected.");
  } catch (err) {
    await client.close();
    throw new Error("Database connection error.");
  }
}

This is our db function that we called inside of our server.mjs to connect us to MongoDB. We then attach it to our app as a variable under app.locals.db. This will allow us to quickly access the database from any of our endpoints under req.app.locals.db.

src/services/firebase.mjs


import admin from "firebase-admin";
import config from "../config/index.mjs";

const serviceAccount = {
  project_id: config.FIREBASE_PROJECT_ID,
  private_key_id: config.FIREBASE_PRIVATE_KEY_ID,
  private_key: config.FIREBASE_PRIVATE_KEY,
  client_email: config.FIREBASE_CLIENT_EMAIL,
  client_id: config.FIREBASE_CLIENT_ID,
  auth_uri: config.FIREBASE_AUTH_URI,
  token_uri: config.FIREBASE_TOKEN_URI,
  auth_provider_x509_cert_url: config.FIREBASE_AUTH_CERT_URL,
  client_x509_cert_url: config.FIREBASE_CLIENT_CERT_URL
};

const firebase = admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

export default {
  auth: firebase.auth()
};

To setup our Firebase Admin SDK to be used, we pass in the certificate information from Firebase that we stored within config file and .env. And then we export the service with invoking auth so its ready to be consumed where ever we import it.

src/middleware/authenticate.mjs


import firebaseAdmin from "../services/firebase.mjs";

export default async function (req, res, next) {
  try {
    const firebaseToken = req.headers.authorization?.split(" ")[1];

    let firebaseUser;
    if (firebaseToken) {
      firebaseUser = await firebaseAdmin.auth.verifyIdToken(firebaseToken);
    }

    if (!firebaseUser) {
      // Unauthorized
      return res.sendStatus(401);
    }

    const usersCollection = req.app.locals.db.collection("user");

    const user = await usersCollection.findOne({
      firebaseId: firebaseUser.user_id
    });

    if (!user) {
      // Unauthorized
      return res.sendStatus(401);
    }

    req.user = user;

    next();
  } catch (err) {
    //Unauthorized
    res.sendStatus(401);
  }
}

This workhorse function will help us validate the Firebase tokens sent from the frontend. Once validated we tack on the user document we fetched from MongoDB onto our request as req.user. On the endpoints we use this middleware, we can always ensure that there’s an authorized user by checking req.user.

src/api/user.mjs


import express from "express";
import authenticate from "../middleware/authenticate.mjs";
import firebaseAdmin from "../services/firebase.mjs";

const router = express.Router();

router.get("/", authenticate, async (req, res) => {
  res.status(200).json(req.user);
});

router.post("/", async (req, res) => {
  const { email, name, password } = req.body;

  if (!email || !name || !password) {
    return res.status(400).json({
      error:
        "Invalid request body. Must contain email, password, and name for user."
    });
  }

  try {
    const newFirebaseUser = await firebaseAdmin.auth.createUser({
      email,
      password
    });

    if (newFirebaseUser) {
      const userCollection = req.app.locals.db.collection("user");
      await userCollection.insertOne({
        email,
        name,
        firebaseId: newFirebaseUser.uid
      });
    }
    return res
      .status(200)
      .json({ success: "Account created successfully. Please sign in." });
  } catch (err) {
    if (err.code === "auth/email-already-exists") {
      return res
        .status(400)
        .json({ error: "User account already exists at email address." });
    }
    return res.status(500).json({ error: "Server error. Please try again" });
  }
});

export default router;

For this example, we are creating two routes in our user.mjs file. The first one gets a user from req.user, which we added in the authentication middleware and sends the document back.

The second one is our sign up route, which creates a new user and adds them to the collection. We do very simple validation on the request body to make sure the fields necessary are there. Much more expansive validation can be done if you want, a good library for that is express-validator. For the sake of this example, we’re not going to use and keep things simple. After validating the body, we then use the Firebase Admin SDK to create the user. This is something that can be done on the frontned, but the reason we do it on the backend is the next piece, relating the Firebase account to our user document in MongoDB. We then return a message to the frontend saying the user was created, or if there’s any errors we send those instead.

Moving on from here, we will take a look at the frontend implementation and how we consume our endpoints and use Firebase to login and protect the information inside our app from those that are unauthorized.