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
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
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
Deleting a document
Deletion of a document is really straight-forward, it’s just simply a case of calling
delete() on the document reference.
Stubbing Firebase for testing
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
Rewire in order to control the libraries the code under test will interact with.
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.
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.
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.
A note from the Plain English team
And as always, Plain English wants to help promote good content. If you have an article that you would like to submit to any of our publications, send an email to firstname.lastname@example.org with your Medium username and what you are interested in writing about and we will get back to you!