Skip to content
Colin Wren
Twitter

Using Firebase for storage and auth with Restify & NodeJS

JavaScript, Software Development, Technology6 min read

fire!
Photo by Adam Wilson on Unsplash

I’ve recently been building an API that implements basic Create, Read, Update, Delete (CRUD) functionality for a CV application I’m going be building.

I built the API consumer first using Pact and once I was happy that my API met the consumer contract with stubs I moved to implement Firebase for storage and auth, using the consumer contract to ensure I didn’t break anything.

It was my first experience using Firebase as I normally fallback to my comfort zone of Django and PostgreSQL if I need to implement authorisation but I’d used Restify in the past and enjoyed it so decided to give Firebase a shot, as it promised easy to implement auth and storage.

Adding Authentication with Firebase

no walking sign
Photo by Siora Photography on Unsplash

Out of the box Firebase supports a pretty impressive number of authentication backends and I think you’d struggle to find a user who doesn’t have at least one account with the backends offered, at time of writing these include:

  • Email address and password
  • Email address and one-time link
  • Phone number
  • Facebook
  • Twitter
  • Google
  • Microsoft
  • Apple

In the following examples I’m using email address and password.

Creating a new user

Before you can log a user in, you need to sign them up for your application.

In order to do this you can use the firebase.auth().createUserWithEmailAndPassword() function which creates the account or throws an error and then use the firebase.auth().currentUser.getIdToken() function to get an auth token for that user.

1async function signupAndGetToken(email, password) {
2 await firebase.auth().createUserWithEmailAndPassword(email, password);
3 return firebase.auth().currentUser.getIdToken();
4}
Wrap a try/catch around this and you’ve got a means of signing the user up and returning their auth token to them

Login an existing user

Once the user has logged into your application for the first time they can then be logged in using the firebase.auth().signInWithEmailAndPassword() function, which, similar to the createUserWithEmailAndPassword() function logs them in but requires the call to getIdToken() to get their auth token.

1async function getTokenForUser(email, password) {
2 await firebase.auth().signInWithEmailAndPassword(email, password);
3 return firebase.auth().currentUser.getIdToken();
4}
Wrap a try/catch around this and you’ve got a means of singing the user in and returning their auth token to them

Getting the user from a JWT

Once the user has signed up or logged in successfully and they’ve got their auth token they’ll then need to use the token to authenticate themselves in subsequent calls to the API, this is often done by using it as a Bearer token in the Authorization header (e.g. Authorization: Bearer [TOKEN] ).

In Restify you can use functions as middleware to be applied to all calls to the server and perform tasks before the controller is called.

I created a function to do this and then applied it using the server.use() function, the middleware functions take the same (req, res, next) arguments as normal controllers.

1const firebase = require('firebase/app');
2const admin = require('firebase-admin');
3const restify = require('restify');
4
5// These take config objects usually
6firebase.initializeApp();
7admin.initializeApp();
8
9function getUserFromJwt(req, res, next) {
10 const authHeader = req.header('Authorization');
11 if (!authHeader) {
12 res.send(401);
13 return false;
14 }
15 try {
16 const authToken = authHeader.replace('Bearer ', '');
17 const decodedToken = await admin.auth().verifyIdToken(authToken);
18 const user = await admin.auth().getUser(decodedToken.sub);
19 req.set('user', user);
20 return next();
21 } catch (e) {
22 res.send(401);
23 return false;
24 }
25}
26
27const server = restify.createServer();
28server.use(getUserFromJwt);
29
30server.listen(8080, '0.0.0.0', () => {
31 console.log(`${server.name} listening at ${server.url}`);
32});
Grab the JWT, get the user from it and set that on the request to be accessed by later controllers

In order to get the user’s record from the JWT you need to call admin.auth().verifyIdToken() which returns the information contained in the token. You can then use the sub property with admin.auth().getUser() to load the user record.

Once I got the user record I used Restify’s req.set() function to set the user object on the request, this can then be used via req.get() in subsequent controllers to access the user record.

GOTCHA — SignOut doesn’t revoke the token

The Firebase documentation suggests that to end the user’s session you call firebase.auth().signOut() but this does not actually revoke the user’s authorisation token, meaning that the user can still access documents for up to an hour after the token they’re using was created.

Instead signOut() tells Firebase to not renew the user’s token should it expire. I’ve not found a means of fully revoking access on logout yet, so this is something to bear in mind when using Firebase for auth.

Using Firebase as storage

books
Photo by Danny on Unsplash

Firebase uses a document store structure. Similar to most NoSQL solutions it’s not bound to a schema and as such can be used to hold varying data between documents.

I’ve not had the opportunity to dive too deeply into the storage side of Firebase yet but from what I’ve seen so far it’s relatively easy to use, but like most NoSQL storage you need to plan ahead for how you’ll scale it out.

Firebase uses collections as a means of separating the document types so if you’re looking to work with a series of documents holding data on dogs for instance you would use admin.firestore().collection('dogs') and then perform the operations against the collection.

Inside the collection there are a series of references to documents; these reference objects are how you interact with creating, deleting, updating and reading the document.

Setting things up

In order to work with Firebase storage you need to initialise the Firestore using admin.firestore() , which returns an object you can use to perform the document operations with.

1const firebase = require('firebase/app');
2const admin = require('firebase-admin');
3
4// These required config objects
5firebase.initializeApp();
6admin.initializeApp();
7const db = admin.firestore();
The db constant will then be used to perform any database operations.

Creating a document

Creating a document is really simple, you just create a new document in the collection using db.collection('dogs').doc() which will return a reference to the newly created document.

That document reference has a method called set() that is used to update the data stored in that document.

1async function saveDoggo(goodBoy) {
2 const doggoRef = await db.collection('dogs').doc();
3 await doggoRef.set(goodBoy);
4}
The document reference is really important for dealing with the record

Searching for a document

Once you’ve created a document it’s likely you’ll want to retrieve it later to interact with it.

While it’s easy enough to do this if you’ve still got the ref stored somewhere it’s often the case that you’ll need to do a search to find it or do a search to a number of documents based on a condition (such as the user ID to get all documents for that user).

In order to search for a document you need to get a reference for the collection the document is stored in, this reference has a method called where() which can be used to provide a predicate to search with.

Once the search is performed by using the get() function provided by where() the resulting object will have a docs property which is an array of the documents returned.

In the following example I do a search for all the documents associated with the user and then return an array of IDs to the user.

1/*
2* Assuming a structure of:
3*{
4* meta: {
5* userId: '',
6* createDate: '',
7* },
8* name: '',
9* breed: '',
10*}
11*/
12
13async function getDoggosForUser(userId) {
14 const doggoCollectionRef = await db.collection('dogs');
15 const doggos = await doggoCollectionRef.where('meta.userId', '=', userId).get();
16 return doggos.docs.map((item) => ({ id: item.id }));
17}
The predicate can search in nested objects using dot syntax

One thing to note is that as Firebase is a NoSQL database. You’ll need to find a means in your schema to store properties that you’ll want to search on such as userId as these won’t be provided by default, in my app I’ve ended up having to extend the schema of my document’s meta object to include the userId in order to search on it.

Reading a document

Directly accessing a document via the document ID in order to read the data from it is managed in a similar manner to performing a search, the only difference being that providing the collection with a document ID means there’s no need to provide a predicate.

You can get the reference to the document using db.collection('dogs').doc(DOCUMENT_ID) which returns the reference to the document in the collection, similar to the search you then need to call get() to get the reference to the document itself.

Once you’ve got the document reference you can use the reference’s exists property to check the document actually exists and call the data() method to read the document data itself.

1async function getDoggoRef(goodBoyId) {
2 const doggoCollectionRef = await db.collection('dogs').doc(goodBoyId);
3 return doggoCollectionRef.get();
4}
5
6async function getDoggoById(goodBoyId) {
7 const doggo = await getDoggoRef(goodBoyId);
8 if (doggo.exists) {
9 return doggo.data();
10 }
11 return {};
12}
I like to split the collection reference logic into a separate function to allow for re-use

Updating a document

Updates to a document are performed on the document ref and are provided as a single depth object.

Any nested object properties that need to be updated need to be provided in dot syntax (e.g. to update the userId property of the meta object you need to provide meta.userId as a key in the update object).

When dealing with the need to use dot syntax for nested objects I found the dotize library useful.

Once you’ve got the update data in the correct format you then call the update() method on the document reference.

The update to the document however does not update the existing document reference so you’ll need to fetch it again and read the data from it if you’re looking to return the updated document to the user.

1const dotize = require('dotize');
2
3
4function formatChangesForUpdate(changes) {
5 return Object.assign({}, ...changes.map((change) => dotize.convert(change)));
6}
7
8
9/**
10* Assumming update format from PATCH request like:
11* const updatesToMake = [
12* {
13* name: 'Prince',
14* },
15* {
16* meta: {
17* userId: 'Me',
18* },
19* },
20*];
21*/
22async function updateDoggoById(goodBoyId, updates) {
23 const doggo = await getDoggoRef(goodBoyId);
24 if (doggo.exists) {
25 await doggo.ref.update(formatChangesForUpdate(updates));
26 const updatedDoggo = await getDoggoRef(goodBoyId);
27 return updatedDoggo.data();
28 }
29 return {};
30}
Dotize is great for taking nested objects and converting them to dot syntax

Deleting a document

Deletion of a document is really straight-forward, it’s just simply a case of calling delete() on the document reference.

1async function deleteDoggoById(goodBoyId) {
2 const doggo = await getDoggoRef(goodBoyId);
3 if (doggo.exists) {
4 await doggo.ref.delete();
5 return true;
6 }
7 return false;
8}
I return true or false as a means of knowing if the document existed but this isn’t necessary

Stubbing Firebase for testing

testing electronics
Photo by Nicolas Thomas on Unsplash

As mentioned at the start of this post, I’ve been using Pact to practise TDD while building my API implementation, and because of the static nature of the pacts created by Pact I need to use stubbed values in my tests.

Even if I wasn’t using Pact however I would still be looking to stub my Firebase calls as this would allow me to test how my application will respond to various scenarios without the need to have a running Firebase to connect to and to manipulate into returning the states I’d need to verify.

I’m using Jest as my test runner because it’s got an incredibly useful auto-mock feature that means that putting modules in a __mock__ directory at the root of the project will apply those modules as stubs for the actual modules imported in the code under test.

This replaces the need to use libraries such as proxyquire and Rewire in order to control the libraries the code under test will interact with.

Mocking firebase-admin

firebase-admin is the module that provides the JWT reading and firestore functionality so it’s where the majority of the firebase calls are made.

The following module needs to be available at __mocks__/firebase-admin.js in order to work, you can use mockImplementation on any of the jest.fn() calls to implement behaviour you want to test.

1const doc = jest.fn().mockImplementation((recordId) => ({
2 get: jest.fn().mockImplementation(() => ({
3 data: jest.fn(),
4 ref: {
5 delete: jest.fn(),
6 update: jest.fn(),
7 },
8 exists: true,
9 })),
10 set: jest.fn(),
11}));
12
13const where = jest.fn();
14
15const auth = jest.fn().mockImplementation(() => ({
16 verifyIdToken: jest.fn(),
17 getUser: jest.fn(),
18}));
19
20const firebaseAdmin = {
21 initializeApp: jest.fn(),
22 firestore: jest.fn().mockImplementation(() => ({
23 collection: jest.fn().mockImplementation(() => ({
24 where: jest.fn(),
25 doc,
26 })),
27 })),
28 auth,
29 credential: {
30 cert: jest.fn(),
31 },
32};
33
34module.exports = firebaseAdmin;
This mock covers all the firebase-admin functionality used in this post

Mocking firebase

firebase/app is the module that provides the functionality to create new user accounts and log them in and out, as well as get the current user. T

hat being the case it’s a relatively lightweight mock.

The following module needs to be available at __mocks__/firebase/app.js in order to work, similar to above you can use mockImplementation to implement behaviour you want to test.

1const auth = jest.fn().mockImplementation(() => ({
2 createUserWithEmailAndPassword: jest.fn(),
3 signInWithEmailAndPassword: jest.fn(),
4 signOut: jest.fn(),
5 currentUser: {
6 getIdToken: jest.fn(),
7 },
8}));
9
10const firebase = {
11 initializeApp: jest.fn(),
12 auth,
13};
14
15module.exports = firebase;
This mock covers all the firebase/app functionality used in this post

Summary

Firebase definitely delivers on it’s promise of easy to implement auth and storage and it’s a pleasure to work with, especially when combined with a modular approach using middleware in Restify and Jest’s auto-mock functionality during test.

I’ve yet to get to a point where I need to worry about Firebase’s pricing model so I haven’t taken that into account but I would recommend Firebase for any developer looking to add auth and/or storage to a project quickly.