DEV Community

Nwakwoke Patrick Nnaemeka
Nwakwoke Patrick Nnaemeka

Posted on • Updated on

Securing your express/Node.js API with Firebase auth

A lot of applications, be it a mobile app or a web app have some form of authentication. If you've worked on various apps, handling authentication can become quite a repetitive task and can get boring which is why I love to make use of external services such as auth0 or firebase to make authentication a breeze. These services can also take care of social auth and that can save us so many lines of code. All we will have to worry about is integration.

In this article, I am going to cover using firebase to secure our APIs so that only authorized users have access to our resources. A common way of securing APIs is with the use of JWT tokens which is generated after a user supplies valid auth credentials and this token is validated on every request. This is quite similar to what we are going to be doing with firebase. We will make use of firebase to handle the generation and validation of this token.

Note that this article is not intended to teach you how to create/start an express server. If you are not familiar with using Node.js or express, I will advise you to check that out before reading this article.

Time for us to dive into some code.

Visit your firebase console and create a new project if you haven't already done that.

The server-side

For the server-side, we will be using the firebase admin SDK as it is more suited for what we are trying to accomplish.

Use this command to install the admin SDK on your server:

npm i firebase-admin

To verify that you are calling APIs from a trusted environment, google recommends you to generate and download a service account key for your project and add it to a path in your environment. So head over to your console, generate a service account key, download it(JSON preferably) and add its location to a path(GOOGLE_APPLICATION_CREDENTIALS) in the environment where you will be running your server.

exports GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-file.json

Look here for more on this.

We can now create a service in our project where we will initialize our SDK with our credentials and export it.

import * as admin from 'firebase-admin';

admin.initializeApp(
  credential: admin.credential.applicationDefault(),
  databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
);

export default admin

Next, we will write the logic that handles the creation of new users. We can place this logic in our auth controller or anywhere you think is suitable.

import admin from './firebase-service';

export const createUser = async (req, res) => {
const {
      email,
      phoneNumber,
      password,
      firstName,
      lastName,
      photoUrl
    } = req.body;

    const user = await admin.auth().createUser({
      email,
      phoneNumber,
      password,
      displayName: `${firstName} ${lastName}`,
      photoURL: photoUrl
    });

    return res.send(user);
}

Now that our logic for creating users is in place. We will need to make sure that requests coming in are from authenticated users. We can achieve this by creating middlewares to protect routes that we want to keep private.

We will create an auth middleware to ensure there is a valid firebase token in the request header.

import admin from './firebase-service';


const getAuthToken = (req, res, next) => {
  if (
    req.headers.authorization &&
    req.headers.authorization.split(' ')[0] === 'Bearer'
  ) {
    req.authToken = req.headers.authorization.split(' ')[1];
  } else {
    req.authToken = null;
  }
  next();
};


export const checkIfAuthenticated = (req, res, next) => {
 getAuthToken(req, res, async () => {
    try {
      const { authToken } = req;
      const userInfo = await admin
        .auth()
        .verifyIdToken(authToken);
      req.authId = userInfo.uid;
      return next();
    } catch (e) {
      return res
        .status(401)
        .send({ error: 'You are not authorized to make this request' });
    }
  });
};


With this middleware in place, the user gets an 'unauthorized' error every time they try to access a private resource without being authenticated.

Now that we have created our middleware, let's use it to protect our private route.

import {Router} from 'express';
import {createUser} from './controllers/authcontroller';
import {checkIfAuthenticated} from './middlewares/auth-middleware';
import {articles} from from './data';

const router = Router();


router.post('/auth/signup', createUser);

router.get('/articles', checkIfAuthenticated, async (_, res) => {
  return res.send(articles);
});  

export default router;


In the code above, we have defined two routes. One for creating our user, the second for fetching articles only if the user is authenticated. Now let's head over to the client-side and see how this API can be consumed.

The client-side

We could be consuming our API using any javascript client-side library or framework for web or mobile apps, so I will not specify any but will rather focus on the firebase javascript SDK. Although there might be some differences in the SDK specific to various javascript libraries/frameworks, the APIs are still quite similar to the official web SDK.

So, we install firebase on the client.

npm i firebase

Note: Your platform might require a different SDK and method of installation, examples are angular-fire and react-native-firebase.

To keep things clean, we can also create a service on our client for initializing firebase with our configurations.

import * as firebase from 'firebase/app';
import 'firebase/auth';

const config = {
  apiKey: "api-key",
  authDomain: "project-id.firebaseapp.com",
  databaseURL: "https://project-id.firebaseio.com",
  projectId: "project-id",
  storageBucket: "project-id.appspot.com",
  messagingSenderId: "sender-id",
  appID: "app-id",
}

firebase.initializeApp(config);

export const auth = firebase.auth

export default firebase;

Your credentials are available on your firebase console. If you're not using javascript on the web you should check out how to initialize firebase on your specific platform.

We will create an auth service for calling the signup endpoint and signing in of users.

import axios from 'axios';
import {auth} from './firebase-service';


export const createUserAccount = (data) => {
  return axios.post('https://your-api-url/auth/signup', data)
    .then(res => res.data)
}


export const loginUser = (email, password) => {
  return auth().signInWithEmailAndPassword(email, password);
}

We have defined our logic for creating a user and logging them into our app. This is how we can check with firebase if a user is already logged in.


firebase.auth().onAuthStateChanged(user => {
   if (user) {
     return user;
   }
});

Now that we have signup and login in place, let us go ahead and generate a token on our client-side for authentication on the server. This can easily be done with a single line of code. Yeah! you heard right, a single line.


const token = await firebase.auth.currentUser.getIdToken();

You can either use it as shown above in an async function or resolve the promise to get the token value. We will be making a request to our API with the token attached to the request header to access the articles resource.

import {auth} from './firebase-service';

export const getArticles = async () => {
const token = await auth.currentUser.getIdToken();

return axios.get('https://your-api-url/articles', {headers:  
  { authorization: `Bearer ${token}` }})
  .then(res => res.data);
}

We've simply passed in our firebase token to the authorization header. it will be extracted on the server-side and used to authenticate our user. This will all be handled by the middleware we created earlier

User roles

One very important part of user authentication is role management. What if we want to have different levels of authorization and restrict access to certain resources to users with certain roles. This is also very easy to implement with firebase authentication.

We will be managing the roles on our server and this is how we can go about it.

import admin from './firebase-service';

export const makeUserAdmin = async (req, res) => {
  const {userId} = req.body; // userId is the firebase uid for the user

  await admin.auth().setCustomUserClaims(userId, {admin: true});

  return res.send({message: 'Success'})
}

Now that we can assign roles to our user, how do we check if a user has a certain role? Easy, when we verify a user's token in our middleware, we can easily access this information on the data that is returned. We will add a middleware that checks if our user has an admin role.

import admin from './firebase-service';

const getAuthToken = (req, res, next) => {
  if (
    req.headers.authorization &&
    req.headers.authorization.split(' ')[0] === 'Bearer'
  ) {
    req.authToken = req.headers.authorization.split(' ')[1];
  } else {
    req.authToken = null;
  }
  next();
};

export const checkIfAdmin = (req, res, next) => {
 getAuthToken(req, res, async () => {
    try {
      const { authToken } = req;
      const userInfo = await admin
        .auth()
        .verifyIdToken(authToken);

      if (userInfo.admin === true) {
        req.authId = userInfo.uid;
        return next();
      }

      throw new Error('unauthorized')
    } catch (e) {
      return res
        .status(401)
        .send({ error: 'You are not authorized to make this request' });
    }
  });
};

We can now protect our admin resources with this middleware. Here are our updated routes

import {Router} from 'express';
import {createUser} from './controllers/authcontroller';
import {checkIfAuthenticated, checkifAdmin} from './middlewares/auth-middleware';
import {articles} from from './data';
import {records} from './data/admin';

const router = Router();

router.post('/auth/signup', createUser);

router.get('/stories', checkIfAuthenticated, async (_, res) => {
  return res.send(articles);
});  

router.get('/admin/records', checkIfAdmin, async (_, res) => {
  return res.send(records);
});

export default router;


Any token without an admin role assigned to it will get an 'unauthorized' error if it tries to access our admin resource.

There is a lot more that can be covered but that is all we will be covering in this article. Hopefully, this is enough push to get you started with firebase auth on the server. You can check out more possibilities by exploring the firebase docs.

Top comments (51)

Collapse
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com • Edited

Really great article. Learned a few things.

What strategy would you recommend we use to securely pass the user's password to the create user Route?

Would just hashing with a salt and always checking against that suffice?

Collapse
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Just pass the raw password to firebase auth. Firebase auth will take care of the hashing

Collapse
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com

But aren’t we sending a post request to the router? Is it a security issue at all to send the plaintext password in a post request?

I understand that Firebase takes care of the password hashing, but isn’t that generally done client side?

Thanks for helping me understand.

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Hashing passwords on the client before sending them to the server? Not necessary.
Even if you are handling authentication yourself, you should still hash your password on the server.
I assume your concern is someone stealing your password, if your app’s security is compromised, then they can also steal the hashed password you are sending through the client. So there is no point really.

Thread Thread
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com

Would one benefit of hashing client side be that, if the app’s security were compromised, then the user’s real password wouldn’t leak?

Sorry to be overly pedantic here I’m just trying to learn.

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

If your authentication logic depends on the server authenticating an already hashed password from the client, then all a hacker needs is that hashed password from the client, the real password isn’t useful to the hacker at this point.

Thread Thread
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com

If they have a plain text password I entered and I am a normal user, wouldn’t the thought be that I’ve reused this email / password combination elsewhere?

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Yeah, but if every app did authentication the same way you are suggesting then their hashed password is still all that will be needed in a case of compromise. Your client code can be accessed on the browser so your hashing algorithm isn’t really hidden. My advice to you is just always have ssl.
Hope this guides you.
stackoverflow.com/questions/371592...

Thread Thread
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com

Thanks for taking the time to answer these quandaries.

Last one: even if an attacker has both access to a hash and the hash function, if that hash function is secure, they still can’t reverse that to get the password, correct?

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

No they can’t

Thread Thread
 
v6 profile image
🦄N B🛡

if every app did authentication the same way you are suggesting then their hashed password is still all that will be needed in a case of compromise.

There are, I think, ways to mitigate this kind of hash re-use. And I think Michael is right about there being some security advantages to interception of a hash vs a plaintext password.

Ideally a variable-salted hash of the passphrase would be signed by a given client's private key specific to the user, the same one used for a mutual TLS session.

It could still be intercepted via a MITM attack, but the attack might then give evidence of tampering.

Collapse
 
aech12 profile image
Alex Howez

I see what you mean, the password is being sent from Client to Server to Firebase Server but as far as I know it's not necessary to hash it at any point.
Hashing is done when you want to store the pass, not send it in HTTP methods.

Collapse
 
seanmclem profile image
Seanmclem • Edited

what is "setUserClaim"? As far as I can tell it only exists in this article.

Collapse
 
emeka profile image
Nwakwoke Patrick Nnaemeka

For assigning roles to users. You can read more on the firebase docs
firebase.google.com/docs/auth/admi...

Collapse
 
seanmclem profile image
Seanmclem

Seems they use setCustomUserClaims, not setUserClaim?

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Thanks for pointing this out. Corrected the typo.

Thread Thread
 
seanmclem profile image
Seanmclem

Thanks, also it seems like on your frontend code that firebase.initialize(config); would now be initializeApp instead of initialize;
For a WebApp atleast.

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Thanks for pointing that out. Will correct it

Thread Thread
 
seanmclem profile image
Seanmclem • Edited

Sorry but,

When you do:
return axios.get('https://your-api-url/articles', {headers:
authorization: 'Bearer '+ token})

You missed a bracket or two, and have not put authorization into another object inside headers. It should probably be

return axios.get('https://your-api-url/articles', {headers: {
authorization: 'Bearer '+ token}})

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Thanks again

Thread Thread
 
seanmclem profile image
Seanmclem • Edited

also, auth is a function
auth().setCustomUserClaims
🙃

Collapse
 
eduardoricardez profile image
Eduardo Ricardez

How can i refresh token when expires

Collapse
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Firebase handles that for you, just call the getIdToken function when you need a token

Collapse
 
iampaoloxd profile image
Paolo

hi i am having trouble with the token because it only live for 1hour. My question is if i call 'getIdToken' on every request, can this cause a performance issue or will it billed me much on firebase ? Thanks

Thread Thread
 
emeka profile image
Nwakwoke Patrick Nnaemeka

You definitely won't be billed more and I haven't had any issues with performance because the request is really fast.

Collapse
 
vignzpie profile image
Vignesh Pai

@emeka , Can you add to it?

Collapse
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Firebase handles that for you, just call the getIdToken function when you need a token

Thread Thread
 
gabrielem profile image
Gabriele Marazzi

this is not 100% correct, to force firebase to refresh the token you need to add a true in the function getIdToken(true) otherwise you get the same token until it expire.

Collapse
 
wisdomabioye profile image
wisdomabioye

Why did you chose to use _ over request or req?
Just curious.

Collapse
 
technoplato profile image
Michael Lustig - halfjew22@gmail.com

I think that’s also a paradigm in python for kind of saying, “I have to declare this variable but it isn’t used.”

Collapse
 
emeka profile image
Nwakwoke Patrick Nnaemeka

Just something about parameters that i have to specify and not use

Collapse
 
wisdomabioye profile image
wisdomabioye

Awesome! Thanks for response

Collapse
 
sundiallabs profile image
sundiallabs

Great and useful article. Out of curiosity why did you opt to use the asych/await instead of the following for the checkIfAuthenticated method? Do you see any issues with this alternative?

checkIfAuthenticated = (req, res, next) => {
getAuthToken(req, res, () => {
try {
const { authToken } = req;
admin
.auth()
.verifyIdToken(authToken)
.then(userInfo => {
req.authId = userInfo.uid;
next();
})
} catch (e) {
return res
.status(401)
.send({ error: 'You are not authorized to make this request' });
}
});
};

Collapse
 
arthurfedotiev profile image
Arthur-Fedotiev

This is a perl I looked for!
I can think of just one minor drawback (subjectively). On FE side you use http for sign-up, which is great, but simultaneously on FE you use firebase auth for sign-in.
When your system decides to migrate from firebase auth to whatever by any reason you will have to make an effort on both FE & BE. Wouldn't it be better to hide auth gateway on the BE, and only allow FE to communicate with your BE endpoints consistently?

Collapse
 
esanchezvz profile image
Esteban Sánchez

What would be the correct way to also include social login like facebook or google?
I'm thinking creating a cloud function that runs whenever a new user is created that communicates with our server to add user to db, and then add the auth().signInWithPopup(prvider) function on the frontend.

However I'm not entirely sure since I'm kind of new to firebase and never really thought to integrate firebase auth on a separate server.

Would this be the correct way to go about implementing this?

Collapse
 
karansh491 profile image
karansh491

I want to implement the same.
Did you successfully implemented it?

Collapse
 
rkast profile image
rkast

Hello! Really, appreciate this succinct guide. Any chance you have repository containing this?

Collapse
 
wisdomabioye profile image
wisdomabioye

Nice article.

Collapse
 
zakariachahboun profile image
zakaria chahboun

Good Article. Thanks!

Collapse
 
kswain1 profile image
Kehlin Swain

How do we avoid sharing app secrets with the client for initializing firebase with our configurations. Do we just use firebase auth and access token?

Collapse
 
rootz491 profile image
Karan Sharma

hey im trying to generate auth token on client side.
but im unable to do so, your one line code to generate token is not working.
please help!

Collapse
 
rinsama77 profile image
imrinzzzz

This is really simple and easy to understand! Thank you!

Collapse
 
raagaware profile image
raagaware

Great article. Thanks mate.

Collapse
 
rush profile image
Aarush Bhat

Hey! I am working on the backend separately, is there a way to generate a token so that I can test the API with postman?