Sijuade Ajagunna Software developer, alt-rock freak. I write sometimes. Interested in learning new things.

Using Firebase Authentication in NestJS apps

11 min read 3211

Introduction

In this article, we’ll be creating a small project that integrates Firebase Authentication into a NestJS application.

Authentication is an essential part of any application, but can be quite stressful to set up from scratch. This is one problem Firebase solves with its authentication product.

Firebase includes a series of products and solutions to make application development easier. Some services provided by Firebase include databases, authentication, analytics, and hosting, among others. Firebase can be integrated into NodeJS apps using the firebase-admin npm module.

NestJS helps you create server-side NodeJS applications using TypeScript. With over 600k downloads per week on npm and 35K stars on GitHub, the framework is a very popular one. It has an Angular-type architecture with features such as controllers and modules. NestJS uses Express under the hood, although it can also be configured to use Fastify.

The project

We’ll create a simple application that allows only authenticated users to gain access to a resource. The users can be authenticated by logging in and signing up through the Firebase client. On authentication, a JSON Web Token (JWT) is provided to the user, which is then sent along with subsequent requests to the restricted resource. The provided JWT is validated on the server side using the firebase-admin SDK and access is allowed or rejected based on the validity of the JWT.

Getting started

First, let’s create a Firebase application. This will provide us with some configurations that we’ll use in our NestJS application later. You can do this via the Firebase console here. Click on Add Project, then name your project. We won’t need Google analytics in this project, so you don’t have to enable it. You can then click on Create Project.

Screenshot of Firebase Create Project screen
Once your application has been created, click on the settings icon just beside Project Overview and select Project Settings. Under the service accounts tab, generate a new private key. This should download a JSON file with some credentials which we’ll use to initialize our Firebase Admin SDK on the server (NestJS) side.

Screenshot of Firebase Project Overview menu

 

In the same Project Settings menu, under the General tab, scroll to Your Apps to register your application with Firebase (if you have already registered an application with Firebase, click the Add App button).

We made a custom demo for .
No really. Click here to check it out.

Our application is web-based, so select the the </> icon. Next, give your application a nickname. You don’t need to select Firebase hosting, unless you plan to do so.

You will be provided some links to scripts as well as Firebase configurations that are required for your application to run properly. Copy the content to a location where you can easily access it as it will be required later.

After this, click on Authentication (located under the Build sidebar), and under the Sign-in method menu, enable Email/Password. We’ll be authenticating users with their email and password.

Initializing your NestJS application

Next, we’ll install the Nest CLI package globally. This will provide us with some commands, one of which is the nest command, which we can use to bootstrap a new NestJS application:

npm i -g @nestjs/cli //install nest cli package globally

nest new firebase-auth-project //create a new nestjs project in a folder named firebase-auth-project

The installation process for creating a new project might take a little while, as all required dependencies need to be installed. The new project should have git initialized with some folders added to .gitignore automatically. Add */**/firebase.config.json to .gitignore.

Start your application in development using the npm run start:dev command. NestJS runs on port 3000 by default, and the server is automatically restarted when a file is saved. Your TypeScript files are compiled into plain JavaScript in the dist folder whenever you start the application.

We’ll be using Handlebars files from the server. To do this, we’ll need the hbs module which can be installed using the following commands:

npm i hbs
npm i @types/hbs

Handlebars is a template engine that helps us write reusable and dynamic HTML. You can read more about template engines here.

You can now modify your main.ts file to look like this:

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import * as hbs from 'hbs';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  const logger = new Logger('App');
  app.useStaticAssets(join(__dirname, '..', 'public'));
  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  hbs.registerPartials(join(__dirname, '..', 'views/partials'));
  app.setViewEngine('hbs');
  app.set('view options', { layout: 'main' });
  await app.listen(3000);
  logger.log('Application started on port 3000');
}

bootstrap();

You may have a Delete`␍` error at the end of each line in your file, especially if you’re running Windows. This is because in Windows, an end-of-line sequence is indicated by CR(carriage-return character) and line breaks, or LF(linefeed character), while git uses only the newline character LF. Running npm run lint should fix the problem, or you can manually set your end-of-line sequence to LF in your code editor.

app.set('view options', { layout: 'main' }); indicates that a main.hbs file will serve as the layout for our hbs files.

There are a couple of packages we’ll use in this project, so let’s get them all installed before going further:

npm i @nestjs/passport class-transformer firebase-admin passport passport-firebase-jwt

Passport is an easy-to-use and hugely popular authentication library for NodeJS, and works very well with NestJS via the @nestjs/passport module to provide a robust authentication system.

Creating routes and hbs files

Let’s create our first routes. In the app.controller.ts file, add the following code:

import { Controller, Get, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller('')
export class AppController {
  constructor(private readonly appService: AppService) {}
  @Get('login')
  @Render('login')
  login() {
    return;
  }

  @Get('signup')
  @Render('signup')
  signup() {
    return;
  }
}

This indicates that when we send a GET request to the /login route, the login.hbs file should be rendered for us, as well as the signup route. Let’s create those hbs files now.

In the root of your project, create public and views folders. Your folder structure should look somewhat like this:

├──-public
├──-src
├───test
├───views

Remember, we have indicated main.hbs to be our layouts file, so inside the views folder, create the main.hbs file and add the following code:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
        integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="/styles/style.css">
</head>
<body>
    <nav class="navbar navbar-dark bg-primary navbar-expand">
        <div class="container"><a class="navbar-brand" href="#">Nest Auth</a>
        </div>
    </nav>
    {{{body}}}
    <div id="quotes" class="d-none">
    </div>
    <script src="https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/8.3.1/firebase-auth.js"></script>
    <script src='/scripts/main.js'></script>
</html>

Notice the first two scripts at the bottom of the file. These are the scripts to use Firebase features on the web. The first is the core FirebaseJS SDK while the second is for Firebase Authentication. You are required to add the scripts for the Firebase features you need in your application.

Create a login.hbs and signup.hbs file in the view folder and add the following code.

login.hbs:

<div class='container'>
    <form id='login-form' class='mt-3'>
        <div class='form-group'>
            <label htmlFor='email'>Email address</label>
            <input type='email' class='form-control' id='email' placeholder='Enter email' required />
        </div>
        <div class='form-group'>
            <label htmlFor='password'>Password</label>
            <input type='password' class='form-control' id='password' placeholder='Password' required />
        </div>
        <p id="error" class="text-white text-small bg-danger"></p>
        <button type='submit' class='btn btn-primary pull-left'>
            Login
        </button>
    </form>
</div>
<script src='/scripts/login.js'></script>

signup.hbs:

<div class='container'>
    <form id='signup-form' class='mt-3'>
        <div class='form-group'>
            <label htmlFor='email'>Email address</label>
            <input type='email' class='form-control' id='email' placeholder='Enter email' required />
        </div>
        <div class='form-group'>
            <label htmlFor='password'>Password</label>
            <input type='password' class='form-control' id='password' placeholder='Password' required />
        </div>
        <p id="error" class="text-white text-small bg-danger"></p>
        <button type='submit' class='btn btn-primary'>
            Signup
        </button>
    </form>
</div>
<script src="/scripts/signup.js"></script>
>

Now for the styles and scripts. In the public folder, add scripts and styles subfolders. Inside the styles subfolder, add a style.css file.

style.css:

blockquote {
  position: relative;
  text-align: left;
  padding: 1.2em 0 2em 38px;
  border: none;
  margin: 20px auto 20px;
  max-width: 800px;
  width: 100%;
  display: block;
}
blockquote:after {
  content: '';
  display: block;
  width: 2px;
  height: 100%;
  position: absolute;
  left: 0;
  color: #66cc66;
  top: 0;
  background: -moz-linear-gradient(
    top,
    #66cc66 0%,
    #66cc66 60%,
    rgba(255, 255, 255, 0) 100%
  );
  background: -webkit-linear-gradient(
    top,
    #66cc66 0%,
    #66cc66 60%,
    rgba(255, 255, 255, 0) 100%
  );
}
blockquote:before {
  content: '\f10d';
  font-family: 'fontawesome';
  font-size: 20px;
  display: block;
  margin-bottom: 0.8em;
  font-weight: 400;
  color: #66cc66;
}
blockquote > cite,
blockquote > p > cite {
  display: block;
  font-size: 16px;
  line-height: 1.3em;
  font-weight: 700;
  font-style: normal;
  margin-top: 1.1em;
  letter-spacing: 0;
  font-style: italic;
}

Inside the scripts folder, create the following files: main.js, login.js, and signup.js. You can leave them empty for now, we’ll come back to them. You should visit the /login and /signup routes to ensure your files are being properly rendered.

Creating our resource

The next item on our list is to create our restricted resource. In this case, it’s going to be a list of quotes and their authors. To create a new resources folder (with module, controller, and service all set up) run:

nest g resource resources

Select REST API as the transport layer and No as the answer to “Would you like to generate CRUD entry points?”

Once done, in the resources.service.ts file, add the following code:

import { Injectable } from '@nestjs/common';

@Injectable()
export class ResourcesService {
  private readonly resources: any[];
  constructor() {
    this.resources = [
      {
        quote: 'They taste like...burning.',
        character: 'Ralph Wiggum',
      },
      {
        quote: 'My eyes! The goggles do nothing!',
        character: 'Rainier Wolfcastle',
      },
      {
        quote:
          "Hello, Simpson. I'm riding the bus today becuase Mother hid my car keys to punish me for talking to a woman on the phone. She was right to do it.",
        character: 'Principal Skinner',
      },
      {
        quote:
          'I live in a single room above a bowling alley...and below another bowling alley.',
        character: 'Frank Grimes',
      },
      {
        quote:
          "All I'm gonna use this bed for is sleeping, eating and maybe building a little fort.",
        character: 'Homer Simpson',
      },
      {
        quote: 'In theory, Communism works! In theory.',
        character: 'Homer Simpson',
      },
      {
        quote: "Oh, wow, windows. I don't think I could afford this place.",
        character: 'Otto',
      },
    ];
  }

  getAll() {
    return this.resources;
  }
}

There you can see our quotes (from the TV show “The Simpsons”) and a method, getAll(), which returns all of them.

Add this to the resources.controller.ts file:

import { Controller, Get } from '@nestjs/common';
import { ResourcesService } from './resources.service';

@Controller('resources')
export class ResourcesController {
  constructor(private readonly resourcesService: ResourcesService) {}

  @Get('')
  getAll() {
    return this.resourcesService.getAll();
  }
}

The @Controller() decorator indicates that routes that start with /resources are directed to this endpoint. We have one GET endpoint that returns all of our quotes using the getAll() method in resources.service.ts. To test your application, sending a GET request to http://localhost:3000/resources should return all quotes.

This endpoint is currently public, and it’s time to work on the authentication part of our application.

Firebase client

To authenticate users from the client side with Firebase, first we initialize our application using the Firebase web configuration provided when you created a new app in your Firebase console. You can get this in the General tab in the project settings menu.

Add the settings to your main.js file in the public folder this way:

const quotes = document.getElementById('quotes');
const error = document.getElementById('error');

var firebaseConfig = {
  apiKey: 'AIzaSyB7oEYDje93lJI5bA1VKNPX9NVqqcubP1Q',
  authDomain: 'fir-auth-dcb9f.firebaseapp.com',
  projectId: 'fir-auth-dcb9f',
  storageBucket: 'fir-auth-dcb9f.appspot.com',
  messagingSenderId: '793102669717',
  appId: '1:793102669717:web:ff4c646e5b2242f518c89c',
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);

const displayQuotes = (allQuotes) => {
  let html = '';
  for (const quote of allQuotes) {
    html += `<blockquote class="wp-block-quote">
                <p>${quote.quote}. </p><cite>${quote.character}</cite>
            </blockquote>`;
  }
  return html;
};

quotes, error, and displayQuotes are variables that will be used by login.js and signup.js scripts, so it is important that your main.js file is imported before the other two. The main.js in turn has access to the firebase variable because the Firebase scripts were first included in the main.hbs file.

Now, to handle user signup, add this to signup.js:

const signupForm = document.getElementById('signup-form');
const emailField = document.getElementById('email');
const passwordField = document.getElementById('password');
signupForm.addEventListener('submit', (e) => {
  e.preventDefault();
  const email = emailField.value;
  const password = passwordField.value;
  firebase
    .auth()
    .createUserWithEmailAndPassword(email, password)
    .then(({ user }) => {
      return user.getIdToken().then((idToken) => {
        return fetch('/resources', {
          method: 'GET',
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${idToken}`,
          },
        })
          .then((resp) => resp.json())
          .then((resp) => {
            const html = displayQuotes(resp);
            quotes.innerHTML = html;
            document.title = 'quotes';
            window.history.pushState(
              { html, pageTitle: 'quotes' },
              '',
              '/resources',
            );
            signupForm.style.display = 'none';
            quotes.classList.remove('d-none');
          })
          .catch((err) => {
            console.error(err.message);
            error.innerHTML = err.message;
          });
      });
    })
    .catch((err) => {
      console.error(err.message);
      error.innerHTML = err.message;
    });
});

And login in login.js:

const loginForm = document.getElementById('login-form');
const emailField = document.getElementById('email');
const passwordField = document.getElementById('password');
loginForm.addEventListener('submit', (e) => {
  e.preventDefault();
  const email = emailField.value;
  const password = passwordField.value;
  firebase
    .auth()
    .signInWithEmailAndPassword(email, password)
    .then(({ user }) => {
      return user.getIdToken().then((idToken) => {
        return fetch('/resources', {
          method: 'GET',
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${idToken}`,
          },
        })
          .then((resp) => resp.json())
          .then((resp) => {
            const html = displayQuotes(resp);
            quotes.innerHTML = html;
            document.title = 'quotes';
            window.history.pushState(
              { html, pageTitle: 'quotes' },
              '',
              '/resources',
            );
            loginForm.style.display = 'none';
            quotes.classList.remove('d-none');
          })
          .catch((err) => {
            console.error(err.message);
            error.innerHTML = err.message;
          });
      });
    })
    .catch((err) => {
      console.error(err.message);
      error.innerHTML = err.message;
    });
});

Firebase-admin

While users can now sign up and log in to our application, our resources route is still open and accessible by anyone. Remember, we installed firebase-admin in our NestJS application. As I mentioned earlier, this package will help verify the JWT token that is sent from the client before allowing or denying the user access to the route.

In the src folder, create a folder named firebase. This will contain all of our Firebase settings. Inside the firebase folder, create a file called firebase.config.json. This will contain the values of the JSON file downloaded when you generated a private key under the service account tab:

{
  "type": "service_account",
  "project_id": "",
  "private_key_id": "",
  "private_key": "",
  "client_email": "",
  "client_id": "",
  "auth_uri": "",
  "token_uri": "",
  "auth_provider_x509_cert_url": "",
  "client_x509_cert_url": ""
}

It is important to keep these values private as some of them are very sensitive.

Next, we’re going to create a Passport strategy for Firebase. A strategy is an authentication mechanism for a particular service (in this case, Firebase) in Passport. Create a firebase-auth.strategy.ts file in the firebase folder and add the following code:

import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Strategy, ExtractJwt } from 'passport-firebase-jwt';
import * as firebaseConfig from './firebase.config.json';
import * as firebase from 'firebase-admin';

const firebase_params = {
  type: firebaseConfig.type,
  projectId: firebaseConfig.project_id,
  privateKeyId: firebaseConfig.private_key_id,
  privateKey: firebaseConfig.private_key,
  clientEmail: firebaseConfig.client_email,
  clientId: firebaseConfig.client_id,
  authUri: firebaseConfig.auth_uri,
  tokenUri: firebaseConfig.token_uri,
  authProviderX509CertUrl: firebaseConfig.auth_provider_x509_cert_url,
  clientC509CertUrl: firebaseConfig.client_x509_cert_url,
};

@Injectable()
export class FirebaseAuthStrategy extends PassportStrategy(
  Strategy,
  'firebase-auth',
) {
  private defaultApp: any;
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    });
    this.defaultApp = firebase.initializeApp({
      credential: firebase.credential.cert(firebase_params),
    });
  }
  async validate(token: string) {
    const firebaseUser: any = await this.defaultApp
      .auth()
      .verifyIdToken(token, true)
      .catch((err) => {
        console.log(err);
        throw new UnauthorizedException(err.message);
      });
    if (!firebaseUser) {
      throw new UnauthorizedException();
    }
    return firebaseUser;
  }
}

What’s happening here? The JWT is extracted as a bearer token from the request header, and our Firebase application is used to verify the token. If the token is valid, the result is returned, else the user’s request is denied and an unauthorized exception is thrown.

If you’re having ESLint errors when you import the Firebase config, add this to your tsconfig.json file: "resolveJsonModule": true.

Integrating the strategy

Right now, our authentication strategy is a standalone function, which isn’t much help. We can make it middleware and integrate it into the endpoints that require authentication, but NestJS has an easier and better way of handling authentication called Guards. We will create a guard to make use of our Firebase strategy, and with a simple decorator, wrap it around the routes that require authentication.

Create a file called firebase-auth.guard.ts and add the following code to it:

import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';

@Injectable()
export class FirebaseAuthGuard extends AuthGuard('firebase-auth') {
  constructor(private reflector: Reflector) {
    super();
  }
  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>('public', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) {
      return true;
    }
    return super.canActivate(context);
  }
}

Next, update your resources.controller.ts file to look like this:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { FirebaseAuthGuard } from 'src/firebase/firebase-auth.guard';
import { ResourcesService } from './resources.service';
@Controller('resources')
export class ResourcesController {
  constructor(private readonly resourcesService: ResourcesService) {}
  @Get('')
  @UseGuards(FirebaseAuthGuard)
  getAll() {
    return this.resourcesService.getAll();
  }
}

You also need to update your app.module.ts file by adding the FirebaseAuthStrategy to the list of providers:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { FirebaseAuthStrategy } from './firebase/firebase-auth.strategy';
import { ResourcesModule } from './resources/resources.module';

@Module({
  imports: [ResourcesModule],
  controllers: [AppController],
  providers: [AppService, FirebaseAuthStrategy],
})
export class AppModule {}

You can test you application again and you’ll find that our resource route is now well protected.

Conclusion

While this is a basic application, you can build on the knowledge to create larger applications that use Firebase Authentication. You can also easily log out a user from the Firebase client by calling firebase.auth().signOut(). This repository is available on Github.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Sijuade Ajagunna Software developer, alt-rock freak. I write sometimes. Interested in learning new things.

One Reply to “Using Firebase Authentication in NestJS apps”

Leave a Reply