Technology Blog

Look deep into latest news and innovations happening in the Tech industry with our highly informational blog.

How to build a role-based Api with firebase authentication

hkis

Introduction

Almost every app requires some level of authorization system. In some cases, validating a username/password set with our Users table is enough, but often, we need a more fine-grained permissions model to allow certain users to access certain resources and restrict them from others. Building a system to support the latter is not trivial and can be very time consuming. In this tutorial, we’ll learn how to build a role-based auth API using Firebase, which will help us get quickly up and running.

Role-based Auth

In this authorization model, access is granted to roles, instead of specific users, and a user can have one or more depending on how you design your permission model. Resources, on the other hand, require certain roles to allow a user to execute it.

rolebased-authentications

pic courtesy: toptal.com

Firebase

Firebase Authentication

In a nutshell, Firebase Authentication is an extensible token-based auth system and provides out-of-the-box integrations with the most common providers such as Google, Facebook, and Twitter, among others.

It enables us to use custom claims which we’ll leverage to build a flexible role-based API.

We can set any JSON value into the claims (e.g., { role: 'admin' } or { role: 'manager' }).

Once set, custom claims will be included in the token that Firebase generates, and we can read the value to control access.

It also comes with a very generous free quota, which in most cases will be more than enough.

Firebase Functions

Functions are a fully-managed serverless platform service. We just need to write our code in Node.js and deploy it. Firebase takes care of scaling the infrastructure on demand, server configuration, and more. In our case, we’ll use it to build our API and expose it via HTTP to the web.

Firebase allows us to set express.js apps as handlers for different paths—for example, you can create an Express app and hook it to /mypath, and all requests coming to this route will be handled by the app configured.

From within the context of a function, you have access to the whole Firebase Authentication API, using the Admin SDK.

This is how we’ll create the user API.

What We’ll Build

So before we get started, let’s take a look at what we’ll build. We are going to create a REST API with the following endpoints:

 

Http Verb Path Description Authorization
GET /users Lists all users Only admins and managers have access
POST /users Creates new user Only admins and managers have access
GET /users/:id Gets the :id user Admins, managers, and the same user as :id have access
PATCH /users/:id Updates the :id user Admins, managers, and the same user as :id have access
DELETE /users/:id Deletes the :id user Admins, managers, and the same user as :id have access

Each of these endpoints will handle authentication, validate authorization, perform the correspondent operation, and finally return a meaningful HTTP code.

We’ll create the authentication and authorization functions required to validate the token and check if the claims contain the required role to execute the operation.

Building the API

In order to build the API, we’ll need:

  • A Firebase project
  • firebase-tools installed

First, log in to Firebase:

firebase login

Next, initialize a Functions project:
firebase init

? Which Firebase CLI features do you want to set up for this folder? ...
(O) Functions: Configure and deploy Cloud Functions

? Select a default Firebase project for this directory: {your-project}

? What language would you like to use to write Cloud Functions? TypeScript

? Do you want to use TSLint to catch probable bugs and enforce style? Yes

? Do you want to install dependencies with npm now? Yes

At this point, you will have a Functions folder, with minimum setup to create Firebase Functions.

At src/index.ts there’s a helloWorld example, which you can uncomment to validate that your Functions works. Then you can cd functions and run npm run serve. This command will transpile the code and start the local server.

firebase

pic courtesy: toptal.com

Notice the function is exposed on the path defined as the name of it at 'index.ts: 'helloWorld'.

Creating a Firebase HTTP Function

Now let’s code our API. We are going to create an http Firebase function and hook it on /api path.

First, install npm install express.

On the src/index.ts we will:

  • Initialize the firebase-admin SDK module with admin.initializeApp();
  • Set an Express app as the handler of our api https endpoint
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as express from 'express';
admin.initializeApp();
const app = express();
export const api = functions.https.onRequest(app);

Now, all requests going to /api will be handled by the app instance.

The next thing we’ll do is configure the app instance to support CORS and add JSON body parser middleware. This way we can make requests from any URL and parse JSON formatted requests.

We’ll first install required dependencies.

npm install --save cors body-parser

npm install --save-dev @types/cors

And then:

//...
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
//...
const app = express();
app.use(bodyParser.json());
app.use(cors({ origin: true }));
export const api = functions.https.onRequest(app);

Finally, we will configure the routes that the app will handle.

//...
import { routesConfig } from './users/routes-config';
//…
app.use(cors({ origin: true }));
routesConfig(app)
export const api = functions.https.onRequest(app);

Firebase Functions allows us to set an Express app as the handler, and any path after the one you set up at functions.https.onRequest(app);—in this case, api—will also be handled by the app. This allows us to write specific endpoints such as api/users and set a handler for each HTTP verb, which we’ll do next.

Let’s create the file src/users/routes-config.ts

Here, we’ll set a create handler at POST ‘/users’

import { Application } from "express";
import { create} from "./controller";
export function routesConfig(app: Application) {
   app.post('/users',
       create
   );
}

Now, we’ll create the src/users/controller.ts file.

In this function, we first validate that all fields are in the body request, and next, we create the user and set the custom claims.

We are just passing { role } in the setCustomUserClaims—the other fields are already set by Firebase.

If no errors occur, we return a 201 code with the uid of the user created.

import { Request, Response } from "express";
import * as admin from 'firebase-admin'
export async function create(req: Request, res: Response) {
   try {
       const { displayName, password, email, role } = req.body

       if (!displayName || !password || !email || !role) {
           return res.status(400).send({ message: 'Missing fields' })
       }
       const { uid } = await admin.auth().createUser({
           displayName,
           password,
           email
       })
       await admin.auth().setCustomUserClaims(uid, { role })
       return res.status(201).send({ uid })
   } catch (err) {
       return handleError(res, err)
   }
}
function handleError(res: Response, err: any) {
   return res.status(500).send({ message: `${err.code} - ${err.message}` });
}

Now, let’s secure the handler by adding authorization. To do that, we’ll add a couple of handlers to our create endpoint. With express.js , you can set a chain of handlers that will be executed in order. Within a handler, you can execute code and pass it to the next() handler or return a response. What we’ll do is first authenticate the user and then validate if it is authorized to execute. If the user doesn’t have the required role, we’ll return a 403.

On file src/users/routes-config.ts:

//...
import { isAuthenticated } from "../auth/authenticated";
import { isAuthorized } from "../auth/authorized";
export function routesConfig(app: Application) {
   app.post('/users',
       isAuthenticated,
       isAuthorized({ hasRole: ['admin', 'manager'] }),
       create
   );
}

Let’s create the files src/auth/authenticated.ts.

On this function, we’ll validate the presence of the authorization bearer token in the request header. Then we’ll decode it with admin.auth().verifyidToken() and persist the user’s uid, role, and email in the res.locals variable, which we’ll later use to validate authorization.

In the case the token is invalid, we return a 401 response to the client:

import { Request, Response } from "express";
import * as admin from 'firebase-admin'
export async function isAuthenticated(req: Request, res: Response, next: Function) {
   const { authorization } = req.headers
   if (!authorization)
       return res.status(401).send({ message: 'Unauthorized' });
   if (!authorization.startsWith('Bearer'))
       return res.status(401).send({ message: 'Unauthorized' });
   const split = authorization.split('Bearer ')
   if (split.length !== 2)
       return res.status(401).send({ message: 'Unauthorized' });
   const token = split[1]

   try {
       const decodedToken: admin.auth.DecodedIdToken = await admin.auth().verifyIdToken(token);
       console.log("decodedToken", JSON.stringify(decodedToken))
       res.locals = { ...res.locals, uid: decodedToken.uid, role: decodedToken.role, email: decodedToken.email }
       return next();
   }
   catch (err) {
       console.error(`${err.code} -  ${err.message}`)
       return res.status(401).send({ message: 'Unauthorized' });
   }
}

Now, let’s create a src/auth/authorized.ts file.

In this handler, we extract the user’s info from res.locals we set previously and validate if it has the role required to execute the operation or in the case the operation allows the same user to execute, we validate that the ID on the request params is the same as the one in the auth token.

import { Request, Response } from "express";
export function isAuthorized(opts: { hasRole: Array<'admin' | 'manager' | 'user'>, allowSameUser?: boolean }) {
   return (req: Request, res: Response, next: Function) => {
       const { role, email, uid } = res.locals
       const { id } = req.params

       if (opts.allowSameUser && id && uid === id)
           return next();

       if (!role)
           return res.status(403).send();

       if (opts.hasRole.includes(role))
           return next();
       return res.status(403).send();
   }
}

With these two methods, we’ll be able to authenticate requests and authorize them given the role in the incoming token. That’s great, but since Firebase doesn’t let us set custom claims from the project console, we won’t be able to execute any of these endpoints. In order to bypass this, we can create a root user from Firebase Authentication Console
Creating a user from the Firebase Authentication Console

adduser-password

pic courtesy: toptal.com

And set an email comparison in the code. Now, when firing requests from this user, we’ll be able to execute all operations.

//...
  const { role, email, uid } = res.locals
  const { id } = req.params
  if (email === 'your-root-user-email@domain.com')
    return next();
//...

Now, let’s add the rest of the CRUD operations to src/users/routes-config.ts.

For operations to get or update a single user where :id param is sent, we also allow the same user to execute the operation.

export function routesConfig(app: Application) {
   //..
   // lists all users
   app.get('/users', [
       isAuthenticated,
       isAuthorized({ hasRole: ['admin', 'manager'] }),
       all
   ]);
   // get :id user
   app.get('/users/:id', [
       isAuthenticated,
       isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }),
       get
   ]);
   // updates :id user
   app.patch('/users/:id', [
       isAuthenticated,
       isAuthorized({ hasRole: ['admin', 'manager'], allowSameUser: true }),
       patch
   ]);
   // deletes :id user
   app.delete('/users/:id', [
       isAuthenticated,
       isAuthorized({ hasRole: ['admin', 'manager'] }),
       remove
   ]);
}

And on src/users/controller.ts. In these operations, we leverage the admin SDK to interact with Firebase Authentication and perform the respective operations. As we did previously on create operation, we return a meaningful HTTP code on each operation.

For the update operation, we validate all fields present and override customClaims with those sent in the request:

//..
export async function all(req: Request, res: Response) {
   try {
       const listUsers = await admin.auth().listUsers()
       const users = listUsers.users.map(user => {
           const customClaims = (user.customClaims || { role: '' }) as { role?: string }
           const role = customClaims.role ? customClaims.role : ''
           return {
               uid: user.uid,
               email: user.email,
               displayName: user.displayName,
               role,
               lastSignInTime: user.metadata.lastSignInTime,
               creationTime: user.metadata.creationTime
           }
       })

       return res.status(200).send({ users })
   } catch (err) {
       return handleError(res, err)
   }
}
export async function get(req: Request, res: Response) {
   try {
       const { id } = req.params
       const user = await admin.auth().getUser(id)
       return res.status(200).send({ user })
   } catch (err) {
       return handleError(res, err)
   }
}
export async function patch(req: Request, res: Response) {
   try {
       const { id } = req.params
       const { displayName, password, email, role } = req.body

       if (!id || !displayName || !password || !email || !role) {
           return res.status(400).send({ message: 'Missing fields' })
       }

       const user = await admin.auth().updateUser(id, { displayName, password, email })
       await admin.auth().setCustomUserClaims(id, { role })
       return res.status(204).send({ user })
   } catch (err) {
       return handleError(res, err)
   }
}
export async function remove(req: Request, res: Response) {
   try {
       const { id } = req.params
       await admin.auth().deleteUser(id)
       return res.status(204).send({})
   } catch (err) {
       return handleError(res, err)
   }
}
//...

Now we can run the function locally. To do that, first you need to set up the account key to be able to connect with the auth API locally. Then run:

npm run serve

In this blog we have covered the basics of Role-based Auth, Firebase, Building the API, Firebase HTTP Function. If you really enjoyed reading it, please checkout our next blog in series to learn more about ‘Deploy the API’, ‘Consuming the API’, ‘Angular App’ and many more features.

Hire Angular Developer from us, as we give you high quality product by utilizing all the latest tools and advanced technology. E-mail us any clock at – hello@hkinfosoft.com or Skype us: “hkinfosoft“.

To develop the custom web app using Angular, please visit our technology page.

Content Source:

  1. toptal.com