Codementor Events

(Updated)Using Firebase-Admin as an Authenticating Middleware in Express.js

Published May 08, 2019

This is the updated version of this post I made in 2017. And since then firebase has undergone a lot of changes and revision. This works as of 2019 and you should be good to go.
You must have heard of the simplicity of Firebase and how it comes as an all in one solution for database management, authenticating and storage. Do you know that you can use Firebase as an authentication middleware and you will not need to store sessions in your database anymore? Today I will talk about writing a middleware for your express application using Firebase-admin alone. Here are the steps required to create a middleware with Firebase.
Create an account on Google: If you do not have an account on google, you can create one here. After creating the account, head over to the Google Firebase Console and create an account if you don't have one. After creating an account, you will need to create a project in Firebase. creating the project will give you a config object that allows you to connect your application to Firebase's database, storage and authentication services. Firebase gives you a service account that allows you to use firebase-admin in your backend.

Screenshot 2019-03-27 at 1.32.17 PM.png

Install Firebase-Admin in Node: Install firebase-admin in your node application by running npm install firebase-admin - save. This will save Firebase Admin in your application dependencies in case you want to run it in another environment.
Create a Firebase config object: create a firebase config file that will initialize your firebase-admin object to be used in the application. This is a singleton class.

{
  "type": "service_account",
  "project_id": "<project-id>",
  "private_key_id": "<private-key-id>",
  "private_key": "<private-key>",
  "client_email": "<client-email>",
  "client_id": "<client-id>",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<firebase-identifier>.iam.gserviceaccount.com"
}

Initialize firebase for your application: after creating the config object and requiring Firebase and its services(database & authentication), you will need to initialize Firebase in your application like so:

require('dotenv').config();
import firebase from 'firebase-admin';
var serviceAccount = require('./firebase-service-account.json');

export default firebase.initializeApp({
  credential: firebase.credential.cert(serviceAccount),
  databaseURL: process.env.FIREBASE_DATABASE_URL
})

Create a controller for authenticating users on the backend using the initialized firebase config file. This assumes that you already handled authenticating users on your client app. You can check the docs for authenticating users.

// import the firebase config into the auth controller
import firebase from '../../firebase';

const firebaseAuth = async (req, res) => {
  try {
    // req.body the payload coming from the client to authenticate the user
    // uid is the firebase uid generated when a user is authenticated on the firebase client
    const userRequest = await firebase.database().ref(`users/${req.body.uid}`).once('value');
    const userPayload = userRequest.val();
    
    if (userPayload) {
      // create tokenClaims if you wish to add extra data to the generated user token
      const tokenClaims = {
        roleId: userPayload.roleId
      }

      // use firebase admin auth to set token claimsm which will be decoded for additional authentication
      await firebase.auth().setCustomUserClaims(user.uid, tokenClaims);
      
      return res.status(200).json({data: tokenClaims});
    } else {
      return res.status(404).json({error: {message: 'No user found'}});
    }
  } catch (error) {
    return res.status(500).json({
      error: { message: 'could not complete auth request'}
    });
  }
}

export default {
  firebaseAuth
}

Create a middleware that verifies the firebase token sent from the request header like so

// Import Firebase Admin initialized instance to middleware
import firebase from '../../firebase';

const roleRanks = {
  superAdmin: 1,
  admin: 2,
  user: 3
};

export const decodeFirebaseIdToken = async (req, res, next) => {
  if (!req.headers.id_token) {
    return res.status(400).json({
      error: {
        message: 'You did not specify any idToken for this request'
      }
    });
  }

  try {
    // Use firebase-admin auth to verify the token passed in from the client header.
    // This is token is generated from the firebase client
    // Decoding this token returns the userpayload and all the other token claims you added while creating the custom token
    const userPayload = await firebase.auth().verifyIdToken(req.headers.id_token);

    req.user = userPayload;

    next();
  } catch (error) {
    return res.status(500).json({
      error
    });
  }
};

// Checks if a user is authenticated from firebase admin
export const isAuthorized = async (req, res, next) => {
  if (req.user) {
    next();
  } else {
    return res.status(401).json({
      error: {
        message: 'You are not authorised to perform this action. SignUp/Login to continue'
      }
    });
  }
};

// Checks if a user has the required permission from token claims stored in firebase admin for the user
export const hasAdminRole = async (req, res, next) => {
  try {
    const roleRequest = await firebase.database().ref('roles').once('value');
    const rolesPayload = roleRequest.val();
    const role = rolesPayload.find((role) => role.id === roleRanks.admin)

    if (req.user.roleId <= role.id) {
      next();
    } else {
      return res.status(403).json({
        error: {
          message: 'You are not permitted to access this resource'
        }
      });
    }
  } catch(error) {
    return res.status(500).json({
      error: {
        message: 'An error occurred while getting user access. Please try again'
      }
    });
  } 
};

Use the middleware in a route: Finally, after creating the middleware, you can use this middleware in a route and see that it works like so:

import {
  hasAdminRole,
  decodeFirebaseIdToken,
  isAuthorized
} from '../controllers/middleware/auth.middleware';

const UserRoute = (router) => {
  // Get all users
  router.route('/users')
    .get(
      decodeFirebaseIdToken,
      isAuthorized,
      hasAdminRole,
      UserController.getAllUsers
    )
}

export default UserRoute;

This is how it all comes together in the entry point file for your express application:

import express from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import routes from './routes';
import cors from 'cors';

const app = express();
const router = express.Router();

const headers1 = 'Origin, X-Requested-With, Content-Type, Accept';
const headers2 = 'Authorization, Access-Control-Allow-Credentials, x-access-token';
const whitelist = [process.env.CLIENT_URL];

const corsOptionsDelegate = (req, callback) => {
  let corsOptions;
  if (whitelist.indexOf(req.header('Origin')) !== -1) {
    corsOptions = { origin: true };
  } else if (process.env.NODE_ENV === 'production') {
    corsOptions = { origin: true };
  } else {
    corsOptions = { origin: false };
  }
  callback(null, corsOptions);
};

// setup body parser
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

// Use express backend routes
routes(router);
const clientHeaderOrigin = process.env.CLIENT_URL;
app.use(cors(corsOptionsDelegate));

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if(whitelist.indexOf(origin) > -1){
    res.header('Access-Control-Allow-Origin', origin);
  } else {
    res.header('Access-Control-Allow-Origin', clientHeaderOrigin);
  }
  
  res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, PATCH, OPTIONS, PUT');
  res.header('Access-Control-Allow-Headers', `${headers1},${headers2}`);
  res.header('Access-Control-Allow-Credentials', 'true');
  
  next();
});

// Add API Routes 
app.use('/api', router);

const port = process.env.PORT || 3000;

// start the app by using heroku port
app.listen(port, () => {
  console.log('App started on port: ' + port);
});

You do not need to use a different package as an authenticating middleware and store sessions in your database.
Don't forget to ask me any question or if there is any part that you do not understand.

Discover and read more posts from Victor Nwaiwu
get started
post comments1Reply
Victor Nwaiwu
6 years ago

Here is a link to the GitHub repository I made of a sample project it is used in: https://github.com/vonvick/firebase-admin-middleware-example