Firebase is great for prototyping projects, especially when you really want to focus on your frontend or mobile app. Plus getting a server up and running from scratch is tedious. So with a couple of clicks, you have a database, a way to authenticate your app and storage, all for free.
But that, still, comes at a cost. The trade-off of how easy and readily available firebase features are is that it locks you in to their ecosystem. I mean, it doesn't matter the type of project, its really uncomfortable adding features to a project that you know at the back of you mind, will be a pain to move to another infrastructure. Like for instance, firebase SDKs make is sooo easy to store stuff into their database solution, you don't need to worry about schema and what not, but rather how the data maps with objects in your app. But it being so easy, means that how you think about you data structure should adhere to their document object model. And for some solutions, data relativity gets very complex where using a relational database would have been ideal.
Take for instance, you come up with a revolutionary to-do application that probably can add tasks before you even think you need to do them. Great idea isn't it. You don't want to spend too much time reinventing the wheel with chores like logging in, registration and a solution like firebase comes in very handy here. That being said, you want to write your server code in python, to leverage some AI library you just read about online, and you really really want to use MySQL. Conflicting I know! You can pretty much plug and play with a buttload of services available out there but choosing which corners to cut and where to put in the effort is a bit tough. And its your responsibility as the millionaire to be. I will however help you cut one corner with this post.
Lets stick to the theme, authentication. We'll use firebase for our client. Either web or native app, it doesn't matter. But for our backend? Anything. I was using(learning) Nestjs at the time I stumbled onto this solution so bare with my code samples. So you client will use any firebase sdk for authentication and your server should be expecting and verifying a Json Web Token (JWT).
Why firebase?
"Why not I just write all the authentication logic by myself?" Fair question to ask. Especially given the amount of online resources available online for implementing this. But authentication is not just logging in, or registering a new account or the funky access token validation.
There's account or email verification,
There's account reset or password retrieval,
There's single sign on solutions i.e. logging in via Google or Twitter and,
There's making sure that multiple sign on options still work together,
There's access token management. The big ol question of storing the token in memory, local storage or as a cookie.
All this could take up half or more of the time you could have spent perfecting your project idea. Firebase already solves all of this, so we'll use that instead.
Yea nah. You don't need firebase on your server.
Again, sticking to the theme. We're building our own backend, or have build our own backend and we only want to tie up authentication. Here's the beauty, there's a section in the firebase docs that goes:
If your backend is in a language not supported by the Firebase Admin SDK, you can still verify ID tokens. First, find a third-party JWT library for your language. Then, verify the header, payload, and signature of the ID token.
We strike gold here. Conventionally, you would have to install the firebase admin sdk to manage how your users are authorized in the backend. For simpler nodejs projects, this is no issue, but I started to struggle trying to use that admin sdk when working with a very opinionated framework, Nestjs. And getting to these docs you realise:...
How it all works
Lets start with the client app. Honestly I don't think it's necessary for me to elaborate on how to setup firebase authentication for your app. There's plenty of tutorials online for that, and their docs are pretty comprehensive. But given enough reasons, I might edit this to include some example or something. When your user signs in, or signs up i.e.
// This is what some method looks like inside my nuxtjs application to log me in :)
await this.$fire.auth.signInWithEmailAndPassword(
this.email,
this.password
);
Upon success, you'll be able to retrieve the token from wherever within your app to make server requests with, along with other properties your app has been granted access to by the user i.e. email, name etc. by listening to firebase events like so:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in. Get what you need here
// you can call user.getIdToken() from here and store it anywhere you want
}
});
Now requests from there will look something like below. It really doesn't matter how you get your token to the database, whether as authorization header or you pass it as a query parameter with your API url as long as it follows the OAuth standard or it is what your server is expecting. A simple authorized request should look something like this
fetch("https://some-api.herokuapp.com/users/me", {
"headers": {
"authorization": "Bearer <your token here>"
},
"referrer": "http://some-app.herokuapp.com/",
"referrerPolicy": "strict-origin-when-cross-origin",
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
});
In this case, firebase stores a cookie in the users browser to persist the session. I advise that you don't persist the access token but rather keep it in memory, and get a fresh one if you loose it. This is because the access token is very short lived as it should, you expose some security vulnerabilities by doing so, and one of the reasons for using firebase in the first place is to have that all handled for us.
That string token you get from firebase is an actual valid JWT, so "thank you firebase but we'll take it from here". Here's how things will essentially work
When a request is sent to your server, you need to follow a couple of steps to validate the firebase token.
Ensure that the signing algorithm is "RS256" and that the signature is valid.
Validate the token payload claims
1. Ensuring that the the signature is valid
There are many ways to achieve this, depending on the choice of your server stack. Here's how you'd typically do it if you were using an npm package like jsonwebtoken :
jwt.verify(token, publicKey, options, function(err, decoded) {
console.log(decoded.foo) // bar
});
where the verify function takes in 1) the token to verify, 2) the public signing key and your options as an object. Follow the link to learn more about this. More on public keys in a moment. You should be able to pass in the required signing algorithm within you options.
For ASP.NET Core users alike, the System.IdentityModel.Tokens.Jwt
package should be sufficient enough to achieve similar results if not the very same. I'd love to provide an example here but I don't think I have a fitting one as I'm still a bit fuzzy on the platform. I do however have a .NET Core public repo with a working jwt example which can be modified to fit this use case for the hopeless looking for a starting point.
Now, the tricky part, and the most important. Getting the public key used to verify the token's signature. Firebase public keys are actually available and accessible from the link https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com as a json object. This json object usually carries two keys as I've seemed to notice, and to get the one that will work for you, you need to use a key ID or (kid) found in you token's header when decoded. Now you'll use the key ID (kid) you get from your decoded token's header to get the public key as you would with any other json object, like so:
const response = await getPublicKeysFromGoogle();
const publicKeys = response;
const [header64] = rawJwtToken.split('.'); // refer to the structure of a jwt
const header = JSON.parse(
Buffer.from(header64, 'base64').toString('ascii'),
);
const thePublicKeyIWant = publicKeys[header.kid];
As a refresher, a jwt is made up of 3 parts when encoded, seperated by dots ( . ). The header, payload and the verify signature part. The snippet above merely splits the token and only grabs the encoded header, decodes it to then grab the kid (the key id). From there, it extracts the public key from the object returned by my helper function getPublicKeysFromGoogle()
which looks like:
async getPublicKeysFromGoogle(): Promise<AxiosResponse<string>> {
const response = await httpService // httpService comes from nextjs, you can use fetch or axios for this
.get(
'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com',
)
.toPromise();
return response.data;
}
There are two problems with my helper function for now. These public keys expire so we can't fetch them once and save them somewhere but you can refresh them by using the value from the max-age in the Cache-Control header of the response from this endpoint. Secondly, we don't want to send a request every time any of our endpoints are hit, it would slow us down for probably 200ms-350ms depending on where you're hosting your server and that is baaaaaad because this is just for verifying a token, excluding the time you will incur as you satisfy the request. To solve this, employ a cache mechanism and modify the little snippet above.
async getPublicKeysFromGoogle(): Promise<AxiosResponse<string>> {
const keys = await cacheManager.get<string>(
jwtConstants.publicTokenCacheKey,
);
if (keys) {
return keys;
}
const response = await this.httpService
.get(
'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com',
)
.toPromise();
const { 'cache-control': cacheControl } = response.headers;
const [, maxAgeString] = cacheControl.split(',');
const [, age] = maxAgeString.split('=');
const ageInt = Number.parseInt(age);
this.cacheManager.set(jwtConstants.publicTokenCacheKey, response.data, {
ttl: ageInt,
});
return response.data;
}
Here's what's different here; I first check the cache for the keys and return those if I find them, if not, continue fetching them from the endpoint. Now from the response headers, I extract the time remaining, in seconds until these keys expire, and set the keys in cache with the same expiry date I got from my headers. This ensures that I only have to re-fetch the keys once they have expired. Now with this, we have verified our signature.
2. Validate the token payload claims
So apart from the signature verification which ensures that the jwt used with the request is issues by Google for real, we need to also validate claims that are in the payload. This ensures that the request isn't being sent with a possibly hijacked token or something. There's a comprehensive list of claims to check listed here at the bottom of the page, under "ID Token Payload Claims" to which I won't bore you with relisting them again.
To sum up
By the time your app has grown to have so many users that you now need to leave firebase and you can focus on reimplementing authentication on your own, you will only need to change a very small part of your servers logic. How token verification is done, as you won't be using Google's public keys anymore, and I guess which claims to validate. and that's rarely any more of a chore compared to the refactor you will need for your front end. But that's one less part of your system you don't need to worry about anymore.
I wrote this because I once spent hours trying to figure it out and with one or two php solutions online which are older than my own knowledge of php lol. I hope this helps at least one person, and if it's more, that'll be great. I generalised most things here hoping to have this piece less technical as possible but it ended up not turning out that way. If there are any additions, suggestions or any clarifications that you need added here please drop me an email at bs.gumede@outlook.com or inbox me on twitter @sduduzo_g . Neutral to positive criticism is very welcome, and will most likely drive edits to better improve the article and my writting too.
I used carbon.now.sh for the cover image
and I used umletino for the diagram.
Top comments (1)
Good post!