In this article, we will explore how to integrate Firebase authentication into a NestJS application. In our NestJS application, we will integrate Firebase to save user credentials, authenticate them during login, and generate a JWT token for user authentication. We will also verify these tokens.
Firebase authentication provides an extra layer of security for our application. Typically, the frontend application directly interacts with Firebase for user registration and email verification, then shares these details with the backend to save the user information in the database.
However, in our current application, we are working on the backend only. We will create endpoints, and our NestJS server will communicate with Firebase.
Project Setup for Firebase Authentication in NestJS
We already have a basic NestJS project. If you’d like to see how to set it up, you can refer to this article for detailed instructions
Since the project is basic and only contains the app module, controller, and service, we will install dependencies
- Swagger :is used for API documentation, allowing us to view our API endpoints and interact with them without needing third-party tools like Postman.
- nestjs/config:This is used for
.env
file support, enabling credential storage and usage. For example, we store the Firebase API key and port number in the.env
file. - firebase-admin:This package is used to access Firebase and leverage its functionalities in the project.
- class-transformer, class-validator:These are used for data validation to check whether the data is appropriate. If there’s a mismatch in data type or missing data, they return validation errors.
- axios: Is used to send API requests to other servers from our NestJS backend. In this application, we will interact with Firebase’s front-end functionalities through Axios.
- rxjs: is a library for reactive programming that allows us to manage and work with asynchronous data streams, which is useful for handling real-time events, observables, and event-driven architecture within the application.
npm install --save @nestjs/config @nestjs/swagger firebase-admin class-transformer class-validator rxjs axios
Update your main.ts
file to integrate Swagger into your project. copy this code and paste it into your main.ts
so we can view and test the endpoints.
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
const config = new DocumentBuilder()
.setTitle('User Authentication')
.setDescription(
'The API details for the User Authentication Demo application using Firebase in the NestJS backend.',
)
.setVersion('1.0')
.addTag('Authentication')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
As we need to integrate Firebase into the project, we will store Firebase credentials and other dependencies in an environment file. Let’s add a .env
file in the project directory (outside of the source folder) and, for now, update it by adding only the port number.
PORT=3000
Update the AppModule by adding the ConfigModule, so we can use the variables from the .env
file.
imports: [ConfigModule.forRoot()],
Update main.ts
await app.listen(process.env.PORT);
Creating a User Resource in NestJS
Now that we have integrated the .env file in NestJS and also set up Swagger for API documentation, our next step is to create a resource named user for all endpoints related to user registration and verification. Simply copy this command and paste it into your project CLI. It will generate a boilerplate for us, including the controller and service. Note that ‘–no-spec’ will not create any spec files.
nest g resource user --no-spec
Configuring Firebase Authentication
This is a crucial step where we will set up a Firebase project. It’s quite simple: log in with your email, open Firebase in your browser, click on “Go to Console,” and then create a new project . For this demonstration, I will create a new project named “User Authentication.” You can refer to the images below for guidance.
As we are authenticating users in our NestJS application with the help of Firebase, we are going to activate the Firebase User Authentication service. On the main screen of the project, if you scroll down a bit, you will find the Firebase Authentication service. Just click on it.
When you click on it, you will be redirected to the Authentication service page, which provides introductory videos and a ‘Get Started’ button. Click the ‘Get Started’ button, and move to the ‘Sign-in Provider’ screens. Here, I am going to choose ‘Sign in with Email and Password,’ but you can choose according to your preferences. The procedure remains the same in most cases.
Enable the ‘Email/Password’ checkbox, and then save. Now, our authentication service is activated in the project
Next, go to Project Settings, then navigate to Service Accounts, and generate a private key. This will download the credentials for Firebase Admin, which are needed for the NestJS backend.
For this project, I am going to move the downloaded file into the project directory
As we are implementing authentication entirely on the NestJS backend, we will create a web app and use the Firebase API key on the backend.
Import these dependencies in the main .ts file to initialize the Firebase Admin app.
import * as firebaseAdmin from 'firebase-admin';
import * as fs from 'fs';
Update your main.ts to initialize the Firebase Admin app. Simply copy this code and paste it into your application as shown in the images.
//firebase ;
const firebaseKeyFilePath =
'./user-authentication-b6fde-firebase-adminsdk-1q08s-74c6d7bcbb.json';
const firebaseServiceAccount /*: ServiceAccount*/ = JSON.parse(
fs.readFileSync(firebaseKeyFilePath).toString(),
);
if (firebaseAdmin.apps.length === 0) {
console.log('Initialize Firebase Application.');
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(firebaseServiceAccount),
});
}
Note: Don’t forget to move the Firebase credentials files into the project directory.
User Registration Workflow
In user authentication, we will first register the user and save their details in the Firebase user list so we can validate their credentials at the time of login.
Creating a User Registration DTO
First, create a register-user.dto.ts file in the dto folder and paste the code copied below. In this code, at the top, you’ll notice I am importing ApiProperty from Swagger. This will help with Swagger documentation when hitting the user registration controller. Other imports such as IsEmail and IsNotEmpty from class validators will help validate the user-entered data, as our registration endpoint can be accessed by anyone. It is always considered best practice to use these kinds of validations before storing data. You can update these fields according to your needs.
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, Length } from 'class-validator';
export class RegisterUserDto {
@ApiProperty({ description: "The user's first name" })
@IsNotEmpty()
@IsString()
firstName: string;
@ApiProperty({ description: "The user's last name" })
@IsNotEmpty()
@IsString()
lastName: string;
@ApiProperty({ description: "The user's email address" })
@IsNotEmpty()
@IsEmail()
email: string;
@ApiProperty({ description: "The user's phone number" })
@IsNotEmpty()
phoneNumber: string;
@ApiProperty({ description: "The user's password" })
@IsNotEmpty()
@Length(8, 20)
password: string;
}
Note: If you are not using Swagger, you can remove the ApiProperty from these fields. Additionally, if you don’t want the validations, you can remove other fields like IsString().
Adding a User Registration Endpoint
Update your register controller as shown by pasting the code from below.
@UsePipes(new ValidationPipe({ transform: true })) This is for user input validation.
If you don’t want to use validation, you can ignore this step if you don’t need to validate user inputs. In this controller, we are simply returning the response from the userService.registerUser function.
@Post('register')
@UsePipes(new ValidationPipe({ transform: true }))
registerUser(@Body() registerUserDTo: RegisterUserDto) {
return this.userService.registerUser(registerUserDTo);
}
Implementing User Registration Logic
Here is the registerUser
method of our UserService. In this method, we use a try-catch block to handle any possible errors from Firebase, and firebaseAdmin.auth().createUser
for user registration to add the new user to Firebase’s authentication list.
async registerUser(registerUser: RegisterUserDto) {
console.log(registerUser);
try {
const userRecord = await firebaseAdmin.auth().createUser({
displayName: registerUser.firstName,
email: registerUser.email,
password: registerUser.password,
});
console.log('User Record:', userRecord);
return userRecord;
} catch (error) {
console.error('Error creating user:', error);
throw new Error('User registration failed'); // Handle errors gracefully
}
}
Please run the project and hit the registration endpoint with valid data. If you receive the user record in response, congratulations! Our first step is complete. You can verify this by checking the user list in the Firebase Authentication service.
User Login Workflow
Our user has been registered successfully, and we have verified this from the Firebase user list. Now, our next step is to log in the user with their credentials. As I mentioned earlier, Firebase expects this login endpoint to be hit directly from the frontend development side, and then send the auth token in the request for verification. However, we will handle it on the backend using an Axios request to log in through NestJS.
Creating a Login DTO
Let’s create a file named login.dto.ts
in the dto
folder, and keep it simple with just two fields: user email and password. Also, add validations to ensure data refinement.
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, Length } from 'class-validator';
export class LoginDto {
@ApiProperty({ description: "The user's email address" })
@IsNotEmpty()
@IsEmail()
email: string;
@ApiProperty({ description: "The user's password" })
@IsNotEmpty()
@Length(8, 20)
password: string;
}
Adding a User Login Endpoint
Let’s update your user controller to handle the login endpoint for processing the POST request from the user. Copy the code below and paste it into your controller. In this controller, we will return the authentication token, expiry time, and refresh token from the user service login method.
@Post('login')
@UsePipes(new ValidationPipe({ transform: true }))
login(@Body() loginDto: LoginDto) {
return this.userService.loginUser(loginDto);
}
Updating the User Service for Authentication
Now let’s define the loginUser
function in the user service. In this function, we are going to return the idToken
, refreshToken
, and expiresIn
. We will be using the Firebase Auth REST API to perform the login, utilizing the API key fetched from the Firebase web app. We will send a POST request and handle any possible errors.
async loginUser(payload: LoginDto) {
const { email, password } = payload;
try {
const { idToken, refreshToken, expiresIn } =
await this.signInWithEmailAndPassword(email, password);
return { idToken, refreshToken, expiresIn };
} catch (error: any) {
if (error.message.includes('EMAIL_NOT_FOUND')) {
throw new Error('User not found.');
} else if (error.message.includes('INVALID_PASSWORD')) {
throw new Error('Invalid password.');
} else {
throw new Error(error.message);
}
}
}
private async signInWithEmailAndPassword(email: string, password: string) {
const url = `https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=${process.env.APIKEY}`;
return await this.sendPostRequest(url, {
email,
password,
returnSecureToken: true,
});
}
private async sendPostRequest(url: string, data: any) {
try {
const response = await axios.post(url, data, {
headers: { 'Content-Type': 'application/json' },
});
return response.data;
} catch (error) {
console.log('error', error);
}
}
Now hit the login endpoint with the valid email and password. If it returns with the auth token and refresh token, we will proceed to the next phase.
Implementing JWT Authentication in NestJS
After a successful login, we have a valid user in our user source. Let’s secure one of the endpoints named findAllUsers
, so it can be accessed only by users with a valid token generated during the login phase.
Implementing the validateRequest Method
The AuthGuard will intercept every request trying to access the findAllUsers
endpoint. This guard has a method to validateRequest
, which will determine whether the current request is legitimate or not and return a boolean result.
Let’s define this method. You can implement it in the user service, or create a separate authentication service for all the steps mentioned above.
In this method, we will first check whether the request has the necessary header. Then, we will check if it has a bearer token. If it has a bearer token, we will verify this token using the firebaseAdmin.auth().verifyIdToken
method to check the validity of the auth token and return true
or an appropriate error message.
async validateRequest(req): Promise<boolean> {
const authHeader = req.headers['authorization'];
if (!authHeader) {
console.log('Authorization header not provided.');
return false;
}
const [bearer, token] = authHeader.split(' ');
if (bearer !== 'Bearer' || !token) {
console.log('Invalid authorization format. Expected "Bearer <token>".');
return false;
}
try {
const decodedToken = await firebaseAdmin.auth().verifyIdToken(token);
console.log('Decoded Token:', decodedToken);
return true;
} catch (error) {
if (error.code === 'auth/id-token-expired') {
console.error('Token has expired.');
} else if (error.code === 'auth/invalid-id-token') {
console.error('Invalid ID token provided.');
} else {
console.error('Error verifying token:', error);
}
return false;
}
}
Update the UserModule and export the service so that it can be used in other modules, such as the AppModule.
exports: [UserService]
Defining the JWT AuthGuard
Lets, create a guards
folder in our root directory, and then create a file named auth.guard.ts
. Next, paste the code from this section into it. This AuthGuard will intercept our requests before they reach the controller and check if the request header contains a valid token.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { UserService } from 'src/user/user.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private userService: UserService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.userService.validateRequest(request);
}
}
Updating the App Module for AuthGuard Integration
Now update our app module and add our AuthGuard to the providers list.
providers: [AppService, AuthGuard],
and over all it should look like this
@Module({
imports: [ConfigModule.forRoot(), UserModule],
controllers: [AppController],
providers: [AppService, AuthGuard],
})
export class AppModule {}
Updating the Controller for Authentication
Now our guard is complete and ready to use. We can apply this guard to the entire controller or specific endpoints. For now, we want to secure only the findAll
endpoint, so update it by adding the line @UseGuards(AuthGuard)
. The AuthGuard will start intercepting all requests.
Additionally, include @ApiBearerAuth()
; this addition is for the Swagger documentation and shows a lock icon, allowing us to paste our bearer token and hit this endpoint.
here is the complete endpoint
@Get()
@UseGuards(AuthGuard)
@ApiBearerAuth()
findAll() {
return this.userService.findAll();
}
Extra Tips for Firebase Authentication NestJS
Refreshing Authentication Tokens
Alright, we have implemented all the steps for authenticating a user, but there’s an issue: our token expires in just one hour. To improve the user experience, you can use the refresh token to get a new ID token without asking the user for their credentials. This process will happen in the background, so the user won’t need to interact
Updating the Controller for Token Refreshing
First, we update the controller. The refresh token controller is simple; we accept the refresh token as a query parameter. Here, I use a query parameter, but you can also accept the refreshToken
in the body using a DTO, as we did previously, and return a response from refreshAuthToken
.
@Post('refresh-auth')
refreshAuth(@Query('refreshToken') refreshToken: string) {
return this.userService.refreshAuthToken(refreshToken);
}
Updating the Service for Token Management
n this updated refresh auth token, we are sending an Axios request to the Firebase Auth APIs and returning the response in the same way, including the ID token, refresh token, and expiration time
async refreshAuthToken(refreshToken: string) {
try {
const {
id_token: idToken,
refresh_token: newRefreshToken,
expires_in: expiresIn,
} = await this.sendRefreshAuthTokenRequest(refreshToken);
return {
idToken,
refreshToken: newRefreshToken,
expiresIn,
};
} catch (error: any) {
if (error.message.includes('INVALID_REFRESH_TOKEN')) {
throw new Error(`Invalid refresh token: ${refreshToken}.`);
} else {
throw new Error('Failed to refresh token');
}
}
}
private async sendRefreshAuthTokenRequest(refreshToken: string) {
const url = `https://securetoken.googleapis.com/v1/token?key=${process.env.APIKEY}`;
const payload = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
};
return await this.sendPostRequest(url, payload);
}
Send the request with a valid refresh token and check if it’s returning a response. Then follow the steps one by one or send your error in the comment section.
Revoking Refresh Tokens in Firebase Authentication
There may be scenarios when you don’t want the frontend or application to refresh the token automatically, such as during sign-out or similar situations. Firebase provides a solution for this: you can revoke the refresh token. This will ensure that the refresh token does not refresh automatically.
firebaseAdmin.auth().revokeRefreshTokens(userid);
Customizing Firebase Authentication Tokens
Sometimes, as users, we need to set additional values in the Firebase auth token. Firebase provides the ability to do this, so we can set these details with the help of the setCustomUserClaims
method available in Firebase Auth. You can update the claims object according to your requirements, and then these details will be easily accessible.
const claims = {
key: 'value'
anotherKey: 'anotherValue',
};
await firebaseAdmin.auth().setCustomUserClaims(userId, claims);
Conclusion
In conclusion, integrating Firebase authentication into a NestJS application not only simplifies user management but also enhances security through efficient token handling and validation. By following the steps outlined, we created a robust backend that registers users, facilitates secure logins, and handles JWT token validation. The added functionality of refreshing tokens ensures a seamless user experience, allowing for uninterrupted access without frequent re-logins.
With Firebase’s scalable infrastructure, our application can manage user data effectively while providing a secure environment. As you continue to explore NestJS and Firebase, consider implementing additional features such as user role management and custom claims to further enhance your application’s functionality.
Feel free to share your experiences or any challenges you face while implementing these features in the comments below!
Frequently Asked Questions (FAQ) about Firebase Authentication in NestJS
1. What is NestJS, and why should I use it for backend development?
NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It uses TypeScript and incorporates modern design patterns, making it suitable for enterprise-level applications. Its modular architecture promotes code organization and reusability.
2. How does Firebase authentication work with NestJS?
Firebase authentication allows you to authenticate users via email, password, and other providers. In a NestJS application, you can integrate Firebase Admin SDK to manage user registrations, logins, and token verifications directly from your backend, enhancing security and control.
3. Why use JWT for user authentication?
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims to be transferred between two parties. They allow secure communication between the client and server, enabling stateless authentication where the server doesn’t need to store session information.
4. How do I refresh an expired JWT token?
You can use the refresh token provided during login to obtain a new ID token without requiring the user to log in again. Implement an endpoint in your NestJS application to handle this refresh process.
5. What happens if a user wants to sign out?
When a user signs out, you should revoke their refresh token in Firebase to prevent further automatic refreshes. This ensures that the user must log in again to obtain a new token.
6. Can I customize Firebase tokens?
Yes, Firebase allows you to customize user tokens by adding custom claims. This can be useful for implementing role-based access control and other user-specific features.
7. Is it possible to validate user input in NestJS?
Absolutely! NestJS provides powerful validation capabilities using the class-validator
package, allowing you to enforce rules on user input before processing it.
8. How can I handle errors when interacting with Firebase?
You can implement try-catch blocks in your service methods to catch and handle errors returned by Firebase. It’s essential to provide meaningful error messages to improve user experience.
9. Where can I find additional resources for learning NestJS and Firebase?
You can refer to the official documentation for NestJS and Firebase for comprehensive guides and tutorials. There are also numerous community forums and online courses available.
Leave a Reply