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.
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.
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.
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.
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).
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.
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 byCR(carriage-return character)
and line breaks, orLF(linefeed character)
, while git uses only the newline characterLF
. Runningnpm run lint
should fix the problem, or you can manually set your end-of-line sequence toLF
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.
hbs
filesLet’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.
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.
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; }); });
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
.
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "Using Firebase Authentication in NestJS apps"
How do i get the current user in a service?