Imagine you’re building a web application that's about to launch. You've carefully designed the user interface, added exciting features, and made sure everything runs smoothly. But as the launch date gets closer, a nagging concern starts to worry you—security. Specifically, how to ensure that only the right users can access the right parts of your application. This is where authentication comes in.
Authentication is the process of verifying who a user is, and it's a critical aspect of web development. In the vast digital landscape, ensuring that users can securely log in and log out of your application is paramount. One slip, and your app could be vulnerable to attacks, putting user data at risk.
Introduction
In this article, we will explore secure authentication in Node.js, using bcrypt.js
to hash passwords and JWT tokens to manage user sessions. By the end, you'll have a solid understanding of how to implement a strong login/logout system, keeping your users’ data safe and secure.
So, let’s embark on this journey to build a bulletproof authentication system, starting from setting up our environment to securing our routes with JWT. Ready to lock down your Node.js app? Let’s get started.
Setting Up Your Node.js Project Environment
First, initialize your Node.js project with npm init -y
, which creates a package.json
file with default settings. Next, install essential packages: express
for setting up the server, mongoose
for managing MongoDB, jsonwebtoken
for handling JWT tokens, bcryptjs
for hashing passwords, dotenv
for environment variables, cors
for enabling Cross-Origin Resource Sharing, cookie-parser
for parsing cookies. Finally, add nodemon
as a development dependency to automatically restart the server when code changes.
1.`npm init -y`
2.`npm install express mongoose jsonwebtoken bcryptjs dotenv cors cookie-parser`
3.`npm install nodemon -D`
Now modify the package.json
file. Add scripts like my code and type.
"scripts": {
"dev": "nodemon backend/index.js",
"start": "node backend/index.js"
},
"type": "module",
Basic Server Setup
Next, we'll set up a basic Express server. Create a file named index.js
. This code initializes Express and creates an instance of the application. We'll then define a route for the root URL ("/") to handle incoming HTTP GET requests. After that, we'll start the server on port 8000, allowing it to listen for incoming requests.
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Server is ready");
});
app.listen(8000, () => {
console.log("Server is running on PORT 8000");
});
Setting Up Basic Authentication Routes
Now, we will create a folder named 'routes' and in that folder we will make make a new file named authRoute.js
and paste the below code to see basics of routes.
In this code snippet, we're setting up routes for different authentication endpoints using Express. First, we import the express library and create a new router instance. Then, we define three GET routes: /signup
, /login
, and /logout
, each responding with a JSON object indicating that the respective endpoint was hit. Finally, we export the router instance as the default export, making it available for use in other parts of the application.
import express from "express";
// Create a new Express router instance
const router = express.Router();
// Define a GET route for the signup endpoint
router.get("/signup", (req, res) => {
// Return a JSON response indicating that the signup endpoint was hit
res.json({
data: "You hit signup endpoint",
});
});
// Define a GET route for the login endpoint
router.get("/login", (req, res) => {
// Return a JSON response indicating that the login endpoint was hit
res.json({
data: "You hit login endpoint",
});
});
// Define a GET route for the logout endpoint
router.get("/logout", (req, res) => {
// Return a JSON response indicating that the logout endpoint was hit
res.json({
data: "You hit logout endpoint",
});
});
// Export the router instance as the default export
export default router;
Now change the index.js
adding the auth route to test out your end points.
import express from "express";
import authRoute from "./routes/authRoutes.js";
const app = express();
app.get("/", (req, res) => {
res.send("Server is ready");
});
app.use("/api/auth", authRoute);
app.listen(8000, () => {
console.log("Server is running on PORT 8000");
});
Now, you can test it in your browser...but I will use Postman for its convenience. You can test all the end points like this.
Similarly you can see the other routes like Logout and SignUp.
So, Our basic app is ready...now make it a robust and a proper authentication system.
Creating the User Model with Mongoose Schema
Now, first ready our mongoDB database. To do that make a folder Model and under that a file User.js
and in this file add Mongoose schema and model for a User
in a mongoDB database. The schema includes fields for username
, fullName
, password
, and email
, each with specified data types and constraints like uniqueness and required status. The password field also has a minimum length of 6 characters.
import mongoose from "mongoose";
// Define the User schema with various fields and their data types
const userSchema = new mongoose.Schema(
{
// The unique username of the user
username: {
type: String,
required: true,
unique: true,
},
fullName: {
type: String,
required: true,
},
// The password of the user (min length: 6)
password: {
type: String,
required: true,
minLength: 6,
},
// The email of the user (unique)
email: {
type: String,
required: true,
unique: true,
},
},
{ timestamps: true }
);
// Create the User model based on the userSchema
const User = mongoose.model("User", userSchema);
// Export the User model
export default User;
Connecting to MongoDB with Mongoose
Now let's connect to our database. We'll create a folder named db and inside it, a file called connectDB.js
. In this file, we'll define an asynchronous function connectMongoDB
that tries to connect to a MongoDB database using Mongoose. It gets the database connection string from the MONGO_URI
environment variable. If the connection is successful, it logs a success message with the host name. If it fails, it logs the error and exits the process with a status code of 1. The function is exported for use in other parts of the application.
import mongoose from "mongoose";
const connectMongoDB = async () => {
try {
// Use the mongoose.connect() method to connect to the database
// using the MONGO_URI environment variable
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error connection to mongoDB: ${error.message}`);
process.exit(1);
}
};
export default connectMongoDB;
Now to use MONGO_URI
we have to make it in .env
file. Here I have used local mongoDB setup connection string. If you want then you can also use mongoDB atlas.
MONGO_URI=mongodb://localhost:27017/auth-test
Signup function
Now make the signup function. For this 1st make a folder controller and there file authController.js
export const signup = async (req, res) => {
try {
const { fullName, username, email, password } = req.body; // Destructuring user input
// Regular expression for email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ error: "Invalid email format" });
}
// Checking if username is already taken
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).json({ error: "Username is already taken" });
}
// Checking if email is already taken
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(400).json({ error: "Email is already taken" });
}
// Validating password length
if (password.length < 6) {
return res
.status(400)
.json({ error: "Password must be at least 6 characters long" });
}
// Generating salt and hashing the password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Creating a new user instance
const newUser = new User({
fullName,
username,
email,
password: hashedPassword,
});
// Saving the new user to the database
await newUser.save();
// Generating JWT token and setting it as a cookie
generateTokenAndSetCookie(newUser._id, res);
// Sending success response with user data
res.status(201).json({
_id: newUser._id,
fullName: newUser.fullName,
username: newUser.username,
email: newUser.email,
});
} catch (error) {
// Handling errors
console.log("Error in signup controller", error.message);
res.status(500).json({ error: "Internal Server Error" });
}
};
First, it extracts fullName
, username
, email
, and password
from the request body. It validates the email format using a regular expression, returning a 400 status if the format is invalid.
Next, the function checks if the username or email already exists in the database. If either is taken, a 400 status with an error message is returned. It also ensures the password is at least 6 characters long, sending another 400 status if this condition isn't met.
The password is then securely hashed using bcrypt
. A new User
instance is created with the provided data and saved to the database.
After saving, the function generates a JWT token, sets it as a cookie, and returns a 201 status with the user's ID, full name, username, and email. If any errors occur, they are logged, and a 500 status is sent with an "Internal Server Error" message.
To make this function active you have to import these
import { generateTokenAndSetCookie } from "../utils/generateToken.js";
import User from "../Model/User.js";
import bcrypt from "bcryptjs";
Notice something? a new thing called generateTokenAndSetCookie...lets see its code...make a folder utils and there generateTokenAndSetCookie.js
.
// Import the jsonwebtoken library
import jwt from "jsonwebtoken";
// Define a function that generates a JWT and sets it as a cookie
export const generateTokenAndSetCookie = (userId, res) => {
// Use the jsonwebtoken library to sign a JWT with the user ID and a secret key
// Set the expiration time of the token to 15 days
const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "15d",
});
res.cookie("jwt", token, {
maxAge: 15 * 24 * 60 * 60 * 1000, //MS
httpOnly: true, // prevent XSS attacks cross-site scripting attacks
sameSite: "strict", // CSRF attacks cross-site request forgery attacks
secure: process.env.NODE_ENV !== "development",
});
};
The **generateTokenAndSetCookie **function creates a JWT and stores it in a cookie for user authentication.
JWT Generation:
The function uses the jsonwebtoken
library to create a JWT. It signs the token with the user's ID and a secret key (JWT_SECRET
from the environment variables), setting it to expire in 15 days.
Setting the Cookie:
The token is then stored in a cookie on the user's browser. The cookie is configured with several security attributes:
- maxAge: Sets the cookie's lifespan to 15 days.
- httpOnly: Ensures the cookie is not accessible via JavaScript, protecting against XSS (Cross-Site Scripting) attacks.
- sameSite: "strict": Prevents CSRF (Cross-Site Request Forgery) attacks by restricting the cookie to be sent only with requests from the same site.
- secure: Ensures the cookie is only sent over HTTPS if the environment is not development, adding an extra layer of security.
So this function ensures that the user's session is both secure and persistent, making it a crucial part of the authentication process.
Here we have to add another environment variable JWT_SECRET
in .env
. You can add any type of mix of number and string like this.
MONGO_URI=mongodb://localhost:27017/auth-test
JWT_SECRET=GBfChCMY8MB7wnymkWhtoD3cRlA1CC5e6iWSBmnuaSg
Now our signUp
function is complete..so make its route now.
import express from "express";
import { signup } from "../controllers/authController.js";
const router = express.Router();
router.post("/signup", signup);
export default router;
ok, now let’s modify our index.js
Here we added some new imports. dotenv: Loads environment variables securely from .env
; express.json(): Parses incoming JSON requests; express.urlencoded({ extended: true }): Parses URL-encoded data; cookieParser: Handles cookies for JWT tokens; connectMongoDB(): Connects to MongoDB for data storage; Routes: /api/auth manages signup, login, and logout.
Here is updated code of index.js
import express from "express";
import dotenv from "dotenv";
import authRoute from "./routes/authRoutes.js";
import connectMongoDB from "./db/connectDB.js";
import cookieParser from "cookie-parser";
dotenv.config();
const app = express();
app.get("/", (req, res) => {
res.send("Server is ready");
});
// Middleware to parse JSON request bodies
app.use(express.json()); // for parsing application/json
// Middleware to parse URL-encoded request bodies (if you're using form data)
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use("/api/auth", authRoute);
app.listen(8000, () => {
console.log("Server is running on PORT 8000");
connectMongoDB();
});
So. now it’s time to test our signup function in Postman. Let’s see if it’s working or not.
So, here is the results.
Here you can see it’s working properly and you can check it your mongoDB database as well.
Login function
Now make the login function. Let’s go again to our authController.js
file
export const login = async (req, res) => {
try {
const { username, password } = req.body; // Destructuring user input
// Finding the user by username
const user = await User.findOne({ username });
// Comparing passwords using bcrypt
const isPasswordCorrect = await bcrypt.compare(
password,
user?.password || ""
);
// If user or password is incorrect, return error
if (!user || !isPasswordCorrect) {
return res.status(400).json({ error: "Invalid username or password" });
}
// Generating JWT token and setting it as a cookie
generateTokenAndSetCookie(user._id, res);
// Sending success response with user data
res.status(200).json({
message: "Logged in successfully",
});
} catch (error) {
// Handling errors
console.log("Error in login controller", error.message);
res.status(500).json({ error: "Internal Server Error" });
}
};
The login controller authenticates a user by verifying their username and password. It first searches for the user in the database using the username. If found, it compares the provided password with the hashed password stored in the database using bcrypt. If the username or password is incorrect, it returns an error response. On successful verification, it generates a JWT token, sets it as a cookie using generateTokenAndSetCookie
, and responds with a success message, indicating the user is logged in successfully.
Let’s add our login route in authRoutes.js
import express from "express";
import { login, signup } from "../controllers/authController.js";
const router = express.Router();
router.post("/signup", signup);
router.post("/login", login);
export default router;
Let’s test it in Postman.
Here you can see it is successfully showing Logged in.
Logout function
Okay. Now the last function i.e. the logout function. Let’s implement this. It’s pretty simple.
export const logout = async (req, res) => {
try {
// Clearing JWT cookie
res.cookie("jwt", "", { maxAge: 0 });
// Sending success response
res.status(200).json({ message: "Logged out successfully" });
} catch (error) {
// Handling errors
console.log("Error in logout controller", error.message);
res.status(500).json({ error: "Internal Server Error" });
}
};
The logout controller securely logs out a user by clearing the JWT cookie from the client's browser using res.cookie
, setting its value to an empty string and its maxAge
to 0, ensuring immediate expiration. Upon successful cookie clearance, it sends a success response with a message indicating the user is logged out successfully. If any error occurs during this process, it catches the error, logs it, and returns an Internal Server Error response.
Add this route to our authRoute.js
import express from "express";
import { login, logout, signup } from "../controllers/authController.js";
const router = express.Router();
router.post("/signup", signup);
router.post("/login", login);
router.post("/logout", logout);
export default router;
okay. Let’s test our last feature, if it’s working fine or not.
Oh!…It’s working super fine. 😃😃
So, now our complete backend of this authentication is ready. 🎉🎉
My npm package
If you don't want to code everything yourself and want a quick solution, I have created an npm package called auth0_package. You can get it from here.
Github Repository
You can get my all above code here in this github repo here.
Now your backend application is complete. In the next blog, I will explain how to integrate this with your frontend. So stay tuned for that 😉😉.
Conclusion
In conclusion, implementing secure authentication in a Node.js application is crucial for protecting user data and ensuring that only authorized users can access specific parts of your application. By using bcrypt.js for password hashing and JWT tokens for session management, you can create a robust login/logout system. This approach not only enhances security but also provides a seamless user experience. Setting up a MongoDB database and using Express for routing further strengthens the backend infrastructure. With these tools and techniques, you can confidently launch your web application, knowing that it is well-protected against unauthorized access and potential security threats.
Top comments (3)
Nice article. Even though I use third party libraries for authentication most of the time, it's always nice to come across articles like these explaining the basics to make sure I still understand how it all works under the hood.
A little suggestion: when you write code blocks you can specify a language identifier for syntax highlighting like
Thank you for your appreciation. I will surely mention the language from next blog. Thank you!
In Node.js, secure authentication can be achieved using bcrypt.js for hashing passwords and JWT (JSON Web Tokens) for session management. During registration, hash the password with bcrypt, and on login, compare it with the stored hash. If matched, generate a JWT for authentication. Use middleware to protect routes by verifying the JWT, and on logout, clear the token from the client side. This approach ensures secure password storage and efficient session handling.