Loading...

Introduction

While it’s simple enough to roll your own auth system in an application, that is not always the most secure. Plus just the act of setting up auth requires many endpoints and custom logic. One of the most valuable time and effort savers is to integrate with a third party auth provider. One of the best free auth providers out there is Firebase, which is owned and maintained Google--so you know its not going anywhere.

Firebase is more commonly used for its NoSQL database and cloud storage. While those are decent in practice (if not a bit pricey), I would argue their Auth API offers a much bigger value to using their service.

In this guide I will walk you through configuring Auth to be used client side and how we can send tokens via HTTP request and authorize them in Node.js to secure our endpoints.

Client Side Service

In our client side code under /services  (or where ever you prefer) we want to create a new file called firebase.js. Before we can use the following code we need to make sure we yarn add firebase or npm install firebase.


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

app.initializeApp(config.firebaseConfig)

const auth = app.auth()

const signUpUser = async (email, password) => {
  try {
    await auth.createUserWIthEmailAndPassword(email, password)
  } catch (err) {
    console.error(err)
    throw new Error(err.message)
  }
}

const logUserIn = async (email, passwor) => {
  try {
    await auth.signInWithEmailAndPassword(email, password)
  } catch (err) {
    console.error(err)
    throw new Error(err.message)
  }
}

const logUserOut = async () => {
  try {
    await auth.signOut()
  } catch (err) {
    console.error(err)
    throw new Error(err.message)
  }
}

const getAuthToken = async () => {
  try {
    const token = await auth.currentUser?.getIdToken(true)
    return token
  } catch (err) {
    throw new Error(err.message)
  }
}

const firebaseService = { auth, signUpUser, logUserIn, logUserOut, getAuthToken }

export default firebaseService

When we initialize app, we’re passing in a config object that is provided to us by Firebase. You will need to create a new app to get the information needed to establish a connection to your instance. Check out the official documentation.

What we’re doing here is creating a few functions that wrap around methods attached to the firebase auth library. The first one lets us take in an email address and password and create a new user with it. The second is how we log a user in. The third is how we log them out, which will delete their auth token and refresh token. The last one is probably the most important, it gets us the user’s auth token.


import React from "react"
import { Redirect, Route, RouteProps } from "react-router-dom"
import { useSelector } from "react-redux"
import { RootState } from "../../../redux/store"
import { isEqual } from "lodash"

const ProtectedRoute = (props) => {
  const { authorized, lastLocation } = useSelector(
    (state: RootState) => state.auth,
    isEqual
  )

  if (!authorized) {
    return < Redirect to="login" />
  }

  if (lastLocation) {
    return < Redirect to={`${lastLocation}`} />
  }

  return < Route {...props} component={props.component} />
}

export default ProtectedRoute

This component uses the Bouncer pattern, pulling our authorized boolean out of Redux state and checking it. If we aren’t authorized, we are redirected to the last location, or simply to the home route or the login page.

When making an HTTP request using a library like Axios, it would look something like this.


const res = axios.post("http://customappurl.com/orders", {
  authorization: `Bearer ${getAuthToken()}`
})

Server Side Service

In our server side code under /services (or where ever you prefer) create a new firebaseAdmin.js file. We also want to make sure we yarn add firebase-admin or npm install firebase-admin so we have the SDK available.


import admin from "firebase-admin"
import config from "../config"

export const serviceAccount: any = {
  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),
  databaseURL: process.env.DATABASE_URL,
})

const firebaseAdmin = {
  firebase,
  admin,
  firestore: firebase.firestore(),
  auth: firebase.auth(),
}

export default firebaseAdmin

As you can see there are quite a few pieces of information needed to get started, mainly in the form of the service account information, which we pass in when initializing our app. Check out the official documentation for more information on getting this initial config.

Here’s an example middleware we can use to authorize our HTTP endpoints with Express. In this example we are using MongoDB through the ORM Mongoose. We store the user’s firebaseID on their local DB, so after getting the firebase user account, we can retrieve any local account information we have in our DB.


import firebaseAdmin from "./firebase";
import User from "../graphql/user/model";

const getUserFromAuthToken = async (token: string) => {
  try {
    const firebaseUser = await firebaseAdmin.auth.verifyIdToken(token)
    const localUser = await User.findOne({ firebaseId: firebaseUser.uid })
    return localUser
  } catch (err) {
    // WE DON'T WANT TO SPAM ERROR MESSAGES SO RETURN NULL
    return null;
  }
}

export default async function verifyAuthToken(req, res, next) {
    try {
    const bearer = req.headers.authorization || ""
    const token = bearer.split(" ")[1]
    const user = await getUserFromAuthToken(token);
    
    if (!user) {
        return res.status(401).json({ error: "Unauthorized" })
    }
    
    return next()
  } catch (err) {
    return null;
  }
}

And that’s all there is to it. You log the user in client side and send their token along. On the backend we check it before allowing someone to access an API route.