Security is an absolute must for any application, whether you are building a simple pet project or a potential unicorn startup.
Therefore, it's important to implement robust authentication systems to not only protect user data but also to provide personalized experiences to users.
In most cases, we tend to build authentication systems from scratch; this has been the de facto approach. However, an alternative and often more efficient method is to utilize existing tools and services that are already up to industry standards.
This way, you don't need to worry about the nitty-gritty details of security implementations, but rather, you can focus your efforts on building the core functionality of your apps.
In this tutorial, we'll explore Firebase Authentication and walk you through the step-by-step process of securing Node.js apps by integrating Firebase authentication providers.
Table Of Contents
- Getting Started With Firebase Authentication in Node.js
- Step 1: Setting Up Your Firebase Project
- Step 2: Configure Firebase Authentication Service
- Step 3: Creating A Node.js Project
- Step 4: Initializing Firebase in Your Node.js Application
- Step 5: Implementing Authentication Features
- Step 6: Handling Basic User Authentication Processes
- Step 7: Handling Account Management Tasks
- Step 8: Defining the API Routes
- Testing Authentication Flow
- Best Practices When Handling Authentication Using Firebase
- Conclusion
Let's get started!
Getting Started With Firebase Authentication in Node.js
Firebase Authentication offers an easy-to-use SDK that simplifies the process of adding authentication features to your Node.js apps. The beauty of this SDK lies in its ability to abstract away complex security implementations, such as password hashing and multi-factor authentication, allowing you to focus on building your application's core functionality.
The SDK supports multiple authentication providers, including email/password combinations, social providers like Google, and more. This ensures that your apps cater to diverse user requirements, enabling them to authenticate with their preferred method.
Prerequisites
Before you get started, make sure you have the following:
-
Basic knowledge of Node.js and its environment setup. If you don't have Node.js installed on your system, make sure you download and install it from the official Node.js website.
Node.js comes bundled with npm (Node Package Manager)—it will be installed automatically with Node.js. You will use it to install a number of libraries that you use in your project.
After completing the installation process, you can run the following commands on your terminal to check if both Node.js and npm are installed correctly:
node --version npm --version
An active Firebase project and familiarity with the Firebase Console. Don't worry too much about this; in the next section, I will walk you through the process of setting up a Firebase project and navigating the Firebase console.
A code editor (VS Code is my go-to IDE), but feel free to use any code editor you're comfortable with.
With these prerequisites in place, you are ready to dive into the project. Let's start by creating a Firebase project.
Step 1: Setting Up Your Firebase Project
To get started, follow these steps to set up a Firebase project:
Head over to Firebase Developer Console homepage, sign in using your Gmail address, and click the Go to Console button to navigate to the console's overview page.
On the console's overview page, click the Create a project button to create a new project. Then, provide a name for your project.
-
Once the project is successfully created, navigate to the project's overview page. You need to register your application on Firebase to generate API keys.
To register an app, click on the Web icon (highlighted in the image below), provide the app name, and click on the Register app button.
-
After registering your Node.js app, Firebase will provide your app's configuration object code. This code includes your API keys and other project-specific details.
Copy this configuration object code; you'll need it later when integrating Firebase SDK into your Node.js application.
Great! You've successfully created a new Firebase project and registered a web application within that project. In the next step, you'll learn how to configure Firebase authentication service in the developer console.
Step 2: Configure Firebase Authentication Service
Once you have registered your application on Firebase, the next step is to configure the Firebase Authentication service.
In your project's overview page, select Authentication section, and click on the Get started button.
Firebase Authentication supports multiple authentication providers, for this tutorial however, we will focus on implementing the native email/password provider to secure your Node.js app.
On the authentication settings page, click on the Sign-in method tab, then click the Email/Password provider button.Then, toggle the Enable button.
Make sure to also enable the Email link feature. Later on, we will make use of this feature to implement email verification and password reset functionality for the authentication system of the Node.js app.
To enable Email link feature, toggle the Enable button in the Email link (passwordless sign in)
section, and then click Save.
After saving the changes, the status of the email/password provider will change to Enabled.
If you want to add a new provider, simply click the Add new provider button.
Now, with these updates, you are ready to jump into the code and implement the authentication functionality in your Node.js app.
Step 3: Creating A Node.js Project
Now, before you dive into the code, let's go over the objectives. Essentially, we need to build a Node.js authentication API that is powered by Firebase SDK. This means we'll create various routes, along with their handler functions to allow users to register, login, logout, verify their emails, as well as reset their passwords.
Instead of building this functionality from scratch, we will tap into the Firebase email/password provider and the SDK's user authentication methods.
To get started, first, create a folder locally to hold your project files. Then, change the current directory to the project's working directory.
mkdir firebase-auth-node
cd firebase-auth-node
Next, go ahead, and initialize npm to create a package.json
file, which will hold the project's dependencies.
npm init --y
Now, you need to install a couple of packages for this project. For the web server, we will create an Express.js app. Go ahead and install Express.js, as well as these additional packages.
npm install express cookie-parser firebase firebase-admin dotenv
The firebase
package allows you to access the Firebase SDK for Node.js and its functionalities. We'll also use firebase-admin
, which provides a server-side interface for interacting with Firebase services, including Authentication.
Now, go ahead, and open your project folder in your preferred code editor to start development.
Step 4: Initializing Firebase in Your Node.js Application
Let's add the Firebase SDK configurations to your Node.js application. To do that, in your root project folder, create a folder called src
, as well as a .env
file—this file will hold your application's environment variables.
Inside the src folder, create a new config
directory. Inside the config directory, add a new file called firebase.js
.
Now, copy and paste the Firebase SDK configuration object code from the Firebase Console to your .env
file as follows:
//file : .env
PORT=5000
FIREBASE_API_KEY=<your-api-key>
FIREBASE_AUTH_DOMAIN=<your-auth-domain>
FIREBASE_PROJECT_ID=<your-project-id>
FIREBASE_STORAGE_BUCKET=<your-storage-bucket>
FIREBASE_MESSAGING_SENDER_ID=<your-messaging-sender-id>
FIREBASE_APP_ID=<your-app-id>
Then, in the config/firebase.js
file, include the following code to initialize Firebase in your Node.js (Express) application:
// file: src/config/firebase.js
require("dotenv").config();
const firebase = require("firebase/app");
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID
};
firebase.initializeApp(firebaseConfig);
With this code, you have successfully initialized Firebase SDK in your application. Now, let's create the Express server. In your src directory, create an app.js
file, and include the following code:
// file: src/app.js
const express = require('express');
const cookieParser = require('cookie-parser');
require("dotenv").config();
const PORT = process.env.PORT || 8080;
const app = express();
app.use(express.json())
app.use(cookieParser())
app.get('/', (req, res) => {
res.send('Hello World');
});
app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`);
});
Go ahead and start the development server.
node src/app.js
You should see the log output indicating that the server is listening on port 5000.
Awesome! With these changes, you have successfully created an Express application and initialized Firebase in your project to access its features.
Let's proceed to implement the authentication features. We need to define endpoints, as well as their handler functions to manage the authentication functionality.
Step 5: Implementing Authentication Features
There are a few things you need to be aware of about how the Firebase SDK handles authentication. The Firebase SDK provides methods attached to the firebase/auth
module to handle various authentication workflows.
Since we're implementing the email/password provider, we'll primarily focus on the methods that allow us to create a new user with an email address and password, log a user in, log them out, and handle user account management tasks such as email verification and password resets.
To access the auth
module and its methods, make the following addition to the config/firebase.js
file:
// file: src/config/firebase.js
const {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
sendEmailVerification,
sendPasswordResetEmail
} = require("firebase/auth") ;
Make sure to export them as follows:
// file: src/config/firebase.js
module.exports = {
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
sendEmailVerification,
sendPasswordResetEmail,
admin
};
Let's start by implementing the handler functions that manage the user authentication processes.
Step 6: Handling Basic User Authentication Processes
You need define controller functions that will manage the requests to register, login, or logout users. To do that, create a controllers
folder. Inside this folder, include a new file called firebase-auth-controller.js
, and then,First, import the following methods:
// file: src/controllers/firebase-auth-controller.js
const {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
sendEmailVerification,
sendPasswordResetEmail
} = require('../config/firebase');
Each of the authentication methods takes a number of parameters, including a mandatory auth
object that needs to be included and passed alongside the requests. To access this object, go ahead and include this code:
// file: src/controllers/firebase-auth-controller.js
const auth = getAuth();
This object is essential for authentication methods because it provides a secure and isolated context for each user's authentication state. By passing it alongside the authentication requests to Firebase backend, you ensure that the operations are performed within the correct authentication context, preventing unauthorized access or manipulation of user data.
Now, there are a couple of ways you can define your controller functions. Essentially, it's really important to keep the codebase concise and readable.
In this case, you can opt to create each handler function independently, or alternatively, bundle all related handlers into a class and export the instance itself.
You can then tap into the class methods to access the handler functions. To do that, start by creating the controller's class. Then, define the user registration handler function:
// file: src/controllers/firebase-auth-controller.js
class FirebaseAuthController {
registerUser(req, res) {
const { email, password } = req.body;
if (!email || !password) {
return res.status(422).json({
email: "Email is required",
password: "Password is required",
});
}
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
sendEmailVerification(auth.currentUser)
.then(() => {
res.status(201).json({ message: "Verification email sent! User created successfully!" });
})
.catch((error) => {
console.error(error);
res.status(500).json({ error: "Error sending email verification" });
});
})
.catch((error) => {
const errorMessage = error.message || "An error occurred while registering user";
res.status(500).json({ error: errorMessage });
});
}
}
module.exports = new FirebaseAuthController();
To register a user, this code will check for the presence of email and password in the request body.
If provided, it utilizes Firebase's createUserWithEmailAndPassword
method to create a new user account( Firebase also provides a dedicated Cloud storage to manage this data—you can view the registered users on the console. Upon successful registration, Firebase's sendEmailVerification
method is invoked to send a verification email.
Notice that we are including the email verification within the registration endpoint. There are various approaches to this, especially when you have a client consuming such an API; it all depends on your app's specific requirements.
For instance, a common scenario would be—on the client-side—the user provides their credentials to register, then they get redirected to a new page, or you update a component to render an email verification prompt where the user provides their email.
Then, Firebase handles verification by sending the user a verification link to their email, such as this one:
So, ideally, you can opt to separate the registration handler and email verification handler function, and then, consume them separately as needed.
But in this case, once the user is registered, we immediately tap into the user's authentication data in the response body, and send a verification link to the email address. Once they click that email, they should be verified (this logic is handled by Firebase).
You should also note that by default, when you use the createUserWithEmailAndPassword
method, it internally performs a sign-in operation after successfully creating the user.
This behavior is a design choice in Firebase Authentication to maintain consistency and simplicity in the authentication flow. This ensures that the newly created user is authenticated and can immediately access protected resources or perform other actions without the need for an additional log-in step.
Now to handle users sign-in's, within the same class, go ahead and include the following login handler. We'll use the signInWithEmailAndPassword
method to allow users to sign in with their credentials:
// file: src/controllers/firebase-auth-controller.js
loginUser(req, res) {
const { email, password } = req.body;
if (!email || !password) {
return res.status(422).json({
email: "Email is required",
password: "Password is required",
});
}
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const idToken = userCredential._tokenResponse.idToken
if (idToken) {
res.cookie('access_token', idToken, {
httpOnly: true
});
res.status(200).json({ message: "User logged in successfully", userCredential });
} else {
res.status(500).json({ error: "Internal Server Error" });
}
})
.catch((error) => {
console.error(error);
const errorMessage = error.message || "An error occurred while logging in";
res.status(500).json({ error: errorMessage });
});
}
There are a couple of things happening in this code. First, it handles user login by extracting email and password from the request body, ensuring both are provided.
Upon a successful login attempt, it retrieves the user's authentication token (Firebase handles the token creation process by default) from the user object returned in the response body, and stores it in an HTTP-only cookie named access_token
.
Why is this important? Well, securing your app's resources is of paramount concern. This means you need to be able to ensure that you only allow access to authenticated and legitimate users.
Firebase SDK's handles JWT token generation, providing you with a variety of tokens, including the access and refresh tokens in the stsTokenManager
object in the response body.
In this case, we are setting the idToken
, which holds information about the authenticated user, in a cookie. Whenever a user makes subsequent requests, the token will be include in the requests, and you can, through a middleware, access the token and validate it.
However, it's important to note that this is just one way of implementing user authentication and protecting your application's resources. You can further enhance the security of your application by implementing additional measures, such as implementing role-based access control.
Now, to handle the token validation process, you need to define a middleware function that will validate the token.
Firebase provides tools for this, but it requires a service account first—a JSON file containing credentials and information necessary for server-to-server interactions with Firebase services. It will grant your Node.js application access to Firebase's administrative features, including token validation.
To access the service account, head over to your Firebase console, click on the Settings icon in the top-left corner of the developer console, and select Project Settings. Then, select the Service Account tab, and click on Generate new private key.
Copy and paste the downloaded JSON file from the download folder into the src
directory of your project, and rename it as follows: FirebaseService.json
.
Then. in the config/firebase.js
file, include the following code:
// file: src/config/firebase.js
const admin = require('firebase-admin');
const serviceAccount = require("../firebaseService.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
This code initializes the Firebase Admin SDK in the Node.js app, providing administrative access to Firebase services, using the service account credentials loaded from the JSON file.
This ensures that the application has the necessary privileges to interact with and manage Firebase resources programmatically, including accessing and validating tokens included in subsequent HTTP requests.
Make sure to export the admin instance in your code.
module.exports = {admin};
Now, create the middleware that will validate the tokens. To do that, create a new middleware
directory in the src
folder, and inside this folder, create a new index.js
file, and include the following code:
// file: src/middleware/index.js
const { admin } = require("../config/firebase");
const verifyToken = async (req, res, next) => {
const idToken = req.cookies.access_token;
if (!idToken) {
return res.status(403).json({ error: 'No token provided' });
}
try {
const decodedToken = await admin.auth().verifyIdToken(idToken);
req.user = decodedToken;
next();
} catch (error) {
console.error('Error verifying token:', error);
return res.status(403).json({ error: 'Unauthorized' });
}
};
module.exports = verifyToken;
This middleware will verify the authenticity of the tokens passed in subsequent requests, ensuring secure access to protected routes. We'll include it in the routes that require authentication.
Finally, include this logout handler to sign out users, and clear the token cookies:
// file: src/controllers/firebase-auth-controller.js
logoutUser(req, res) {
signOut(auth)
.then(() => {
res.clearCookie('access_token');
res.status(200).json({ message: "User logged out successfully" });
})
.catch((error) => {
console.error(error);
res.status(500).json({ error: "Internal Server Error" });
});
}
This function will effectively logs out a user, clear their access token cookie, and sends an appropriate response.
Step 7: Handling Account Management Tasks
Now, let's implement a handler function that will allow users to manage and reset their passwords. Firebase will take care of the underlying complexities, from generating the email reset link and sending it, to updating the password.
To do that, include this resetPassword
controller in the controllers class:
// file: src/controllers/firebase-auth-controller.js
resetPassword(req, res){
const { email } = req.body;
if (!email ) {
return res.status(422).json({
email: "Email is required"
});
}
sendPasswordResetEmail(auth, email)
.then(() => {
res.status(200).json({ message: "Password reset email sent successfully!" });
})
.catch((error) => {
console.error(error);
res.status(500).json({ error: "Internal Server Error" });
});
}
The sendPasswordResetEmail
method will send a password reset email to the specified email address. Users can then update their update their passwords through Firebase's UI page rendered on the browser.
Step 8: Defining the API Routes
Now, create a new routes
folder. Inside this folder, add a new index.js
file, and include the following contents:
// file: src/routes/index.js
const express = require('express');
const router = express.Router();
const firebaseAuthController = require('../controllers/firebase-auth-controller');
router.post('/api/register', firebaseAuthController.registerUser);
router.post('/api/login', firebaseAuthController.loginUser);
router.post('/api/logout', firebaseAuthController.logoutUser);
router.post('/api/reset-password', firebaseAuthController.resetPassword);
module.exports = router;
Then, import the routes object in the app.js file as follows:
// file: src/app.js
const router = require("./routes");
app.use(router);
Great! Let's proceed to testing your API's authentication functionality.
Testing Authentication Flow
Go ahead and restart the development server.
node src/app.js
There are several testing approaches you can take to ascertain the functionality of the auth API. One way would be to automate the testing using unit tests with tools like Jest and Supertest. Alternatively, you can make use of API clients like Postman or the Thunder Client VS Code extension to test the API.
In this case, we'll make use of Postman to test the various endpoints. For starters, assuming you want to register a new user using their email and password, go ahead and make a POST request and pass these values to this endpoint: http://localhost:5000/api/register
.
You should receive a successful registration status message along with the email verification message. Check your inbox for the email, and click the link provided to verify your email address.
The good thing is that Firebase provides default UI pages for such processes. However, depending on your implementation, feel free to customize the pages to match your app's designs.
Now, with the same credentials, make a POST request to the http://localhost:5000/api/login
endpoint to test the login endpoint.
Once a user successfully logins with their credentials, a user object with various user-specific information is returned. Assuming they had verified their email after registration, the response will include an emailVerified key with a true value. You can then use this information to allow them to access certain features or resources in your application.
Moreover, once they log in, the access token will be set in the cookies as follows:
Once this is set, it will be automatically included in subsequent requests in the app to access protected resources.
For instance, assuming you want to build a blog and render a list of posts to authenticated users, go ahead, and create a new controllers/post-controller.js
file. Then, define the following controller that should return a list of user posts:
// file: src/controllers/post-controller.js
class PostsController {
getPosts(req, res) {
const usersPosts = {
"1": "Post 1",
"2": "Post 2",
"3": "Post 3",
};
res.send(usersPosts);
}
}
module.exports = new PostsController();
Next, include this route, along with the verifyToken
middleware you created earlier to validate the token passed alongside the requests.
// file: src/routes/index.js
const verifyToken = require('../middleware');
const PostsController = require('../controllers/posts-controller.js');
router.get('/api/posts', verifyToken, PostsController.getPosts);
This way, once you successfully log in and make a GET request to access the list of posts through this endpoint: http://localhost:5000/api/posts
, you should receive the following data as a response:
Remember, this is just a basic implementation. Ideally, the PostsController
should be trying to access your app's resources, including data in the database, and return it to the user on the client-side app.
Now, assuming a user wants to reset their password, Firebase's sendPasswordResetEmail
takes the auth
object and email as parameters, and similar to the email verification method, it sends a password reset email containing a link that allows you to access the password reset UI page.
Click the link provided in the password reset email to access the Firebase default password reset UI page to reset and save the new password.
Note that when you want to update the password using the Firebase Authentication reset password method, you should use the POST HTTP method to send the request, instead of the PATCH method. Ideally, Firebase's reset password flow typically involves the following steps:
- The client sends a POST request to a server endpoint (e.g., /api/reset-password) with the user's email address in the request body.
- The server receives the request and calls the
sendPasswordResetEmail
function from the Firebase Authentication SDK, passing the provided email address.
In this flow, the client is not updating a specific resource on the server. Instead, the client is initiating a password reset process by sending a POST request with the user's email address. The server then triggers the Firebase Authentication password reset flow, which is handled entirely by Firebase.
The PATCH
method, however, is typically used when you want to update a specific resource on the server, such as a user's profile data or a specific document in a database. However, in the case of the Firebase Authentication reset password method, there is no specific resource being updated on the server side. The server is simply acting as an intermediary to initiate the password reset process with Firebase.
Best Practices When Handling Authentication Using Firebase
When handling authentication using Firebase, it's essential to follow best practices to ensure secure and efficient authentication systems. Here are some key points to consider:
-
Securing Firebase API keys in Node.js applications is a critical step to maintain the security and integrity of your application.
The recommended approach is to store sensitive information like Firebase API keys and configuration details as environment variables on your server or hosting platform. This prevents these sensitive values from being exposed in your codebase, reducing the risk of accidental leaks or unauthorized access.
-
Firebase authentication provides a robust and scalable authentication system that supports various authentication providers.
However, it's also essential to implement proper user management practices, such as handling authentication states appropriately in your application, including implementing secure storage mechanisms for user tokens (e.g., encrypted cookies or local storage), as well as handling token refresh when necessary to maintain uninterrupted access for the user.
And when users logout or sessions expire, revoke access tokens to prevent unauthorized use of user accounts. -
When handling errors related to authentication and user management, implement proper error handling mechanisms that capture and log errors, while also, displaying user-friendly messages and feedback UI screens to guide users through appropriate steps to resolve issues, as well as provide a great user experience.
Avoid logging raw error messages or stack traces (especially in production), as they may contain sensitive information that could be utilized for malicious purposes.
Furthermore, make sure to implement input validation and sanitization to prevent common security vulnerabilities like SQL injection, cross-site scripting (XSS), and other types of attacks.
Conclusion
In this article, we covered the essential steps to integrate Firebase Authentication into a Node.js application, covering the implementation of user registration, login, logout, and password reset functionality using Firebase Authentication SDK.
If you're interested in learning more about Firebase Authentication and its integration with Node.js, here are some valuable resources:
You can find this project's code in this GitHub repository.
Firebase offers a comprehensive suite of services beyond just Authentication, including Realtime Database, Cloud Firestore, Cloud Functions, and more. We encourage you to explore these services to build scalable, secure, and feature-rich applications.
Top comments (0)