Introduction to the

In this article, we will create a small project to integrate Firebase authentication into a NestJS application.

Authentication is an important part of any application, but setting it up from scratch can be stressful. This is a problem that Firebase solves with its certified product.

Firebase includes a range of products and solutions that make application development easier. Some of the services provided by Firebase include databases, authentication, analysis, hosting, and more. Firebase can be integrated into NodeJS applications via the FireBase-AdminNpm module.

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

The project

We will create a simple application that allows only authenticated users to access a resource. Users can authenticate by logging in and registering on the Firebase client. At authentication time, the user is presented with a JSON Web Token (JWT), which is then sent along with subsequent requests for restricted resources. The supplied JWT is validated on the server side using the Firebase-admin SDK and allows or denies access based on the validity of the JWT.

Begin to use

First, let’s create a Firebase application. This will give us some configuration that we will use in future NestJS applications. You can do this from the Firebase console. Click Add Project and name your project. We don’t need Google Analytics for this project, so you don’t have to enable it. Then you can click Create project.



Once your application is created, clickAn overview of the projectNext to the Settings icon, selectproject Set up the. Under the service account TAB, generate a new private key. This should download a JSON file with some certificates that we will use to initialize our Firebase Admin SDK on the server (NestJS) side.

From the same project Settings menu, under the general TAB, scroll to your app and register your app with Firebase (if you have already registered an app with Firebase, click the Add App button).

Our application is web-based, so select the **** icon. Next, give your application a nickname. You do not need to choose Firebase hosting unless you intend to do so.

You’ll get links to some scripts and the Firebase configuration that your application needs to run properly. Copy this content to a place where you can easily access it, as you will need it later.

After that, click On Authentication (located under the Build sidebar) and under the Login Method menu, enable email/Password. We will authenticate with the user’s email and password.

Initialize your NestJS application

Next, we’ll install the Nest CLI package globally. This will give us a few commands, one of which is the Nest command, which we can use to launch 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-projectCopy the code

The installation process for creating a new project can take a little time, because all the required dependencies need to be installed. New projects should have Git initialization and some folders automatically added to.gitignore. Add */**/firebase.config.json to.gitignore.

Use the NPM run start:dev command to start your application in development. NestJS runs on port 3000 by default, and the server restarts automatically when the file is saved. Whenever you launch the application, your TypeScript files are compiled into pure JavaScript in the Dist folder.

We will use the Handlebars file on the server. To do this, we need the HBS module, which can be installed with the following command.

npm i hbs
npm i @types/hbs

Copy the code

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

You can now modify your main.ts file to look something 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();Copy the code

There may be a Delete ‘␍’ error at the end of every line in your file, especially if you are running Windows. This is because in Windows, end-of-line sequences are represented by CR(carriage return character) and linefeed character, or LF(linefeed character), while git uses only LF. Running NPM run Lint should solve this problem, or you can manually set the 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 file.

We’ll be using several packages in this project, so let’s get them all installed before we go any further.

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

Copy the code

Passport is an easy to use and very popular NodeJS authentication library and works well with NestJS through the @nestjs/ Passport module to provide a powerful authentication system.

Create routes andhbsfile

Let’s create our first route. 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; }}Copy the code

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 registered route. Now let’s create these HBS files.

In the root directory of your project, create the public and Views folders. Your folder structure should look something like this.

├ ─ ─ - public ├ ─ ─ - SRC ├ ─ ─ ─ the test ├ ─ ─ ─ viewsCopy the code

Remember, we already indicated that main. HBS is our layout file, so in the view 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>Copy the code

Notice the first two scripts at the bottom of the file. These are scripts that use Firebase functionality on the network. The first is the core FirebaseJS SDK, and the second is for Firebase authentication. You need to add scripts for the Firebase functionality you need in your application.

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

The 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>Copy the code

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> >Copy the code

Now styles and scripts. In the Public folder, add the scripts and styles subfolders. In the Styles subfolder, add a style.css file.

Style. The 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.8 em. font-weight: 400; color: #66cc66; } blockquote > cite, blockquote > p > cite { display: block; font-size: 16px; The line - height: 1.3 em. font-weight: 700; font-style: normal; Margin - top: 1.1 em. letter-spacing: 0; font-style: italic; }Copy the code

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

Create our resources

The next item on our list is to create our constrained resources. In this case, it would be a list of quotes and their authors. To create a new Resources folder (modules, controllers, and services are set up), run.

nest g resource resources

Copy the code

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

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

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; }}Copy the code

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

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(); }}Copy the code

The @Controller() decorator indicates that routes starting with /resources will be directed to this endpoint. We have a GET endpoint that uses the getAll() method to return all of our quotes in resources.service.ts. In order to test your application, send a request to GET to the [http://localhost:3000/resources] (http://localhost:3000/resources) should return all the quotation.

This endpoint is now exposed, and it’s time to deal with the authentication part of our application.

Firebase client

To authenticate the user from the client with Firebase, we first initialize our application with the Firebase network configuration that you provide when you create a new application on the Firebase console. You can get this in the general TAB of the Project Settings menu.

This adds the Settings to the main.js file in your public folder.

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;
};

Copy the code

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 files. In turn, main.js can access firebase variables because the Firebase script is first contained in the main.hbs file.

Now, to handle user registration, 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;
    });
});

Copy the code

And login to 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;
    });
});

Copy the code

Firebase-admin

While users can now register and log into our application, our Resources path remains open for anyone to access. Remember, we have Firebase-admin installed in our NestJS application. As I mentioned earlier, this package will help validate the JWT token sent from the client and then allow or deny the user access to the route.

In the SRC folder, create a folder named Firebase. This will contain all of our Firebase Settings. In the Firebase folder, create a file called firebase.config.json. This will contain the value of the JSON file you downloaded when generating the 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": ""
}

Copy the code

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

Next, we will create a Passport policy for Firebase. The policy is the authentication mechanism for a particular service in Passport (Firebase in this case). 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;
  }
}

Copy the code

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

If you encounter ESLint errors when importing Firebase configuration, add this to your tsconfig.json file: “resolveJsonModule”: true.

Integrated strategy

Right now, our authentication policy is a separate function, which doesn’t help. We can make it middleware and integrate it into the endpoints that require authentication, but NestJS has a simpler and better way of handling authentication called Guards. We’ll create a guard to take advantage of our Firebase policy and wrap it in a route that requires authentication with a simple decorator.

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); }}Copy the code

Next, update your resources.controller.ts file to look something 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(); }}Copy the code

You will also need to update your app.module.ts file to add 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 {}

Copy the code

You can test your application again and you will see that our resource routing is now well protected.

conclusion

Although this is a basic application, you can build on this knowledge to create larger applications using Firebase authentication. You can also easily log out a user from the Firebase client by calling firebase.auth().signout (). The repository can be found on Github.

The postUsing Firebase Authentication in NestJS appsappeared first onLogRocket Blog.