Implementing JWT Authentication in Node.js with Mongoose

November 2, 2024 (2w ago)

Implementing JWT Authentication in Node.js with Mongoose

JWT (JSON Web Token) authentication is a secure, stateless method for handling user authentication in web applications. Unlike traditional sessions stored on the server, JWTs allow you to manage authentication directly in the client, making your application more scalable. In this guide, we’ll walk through implementing JWT authentication in a Node.js application using Express and Mongoose, covering everything from token generation to securing routes.


What is JWT Authentication?

JWT is a secure, stateless token format commonly used in REST APIs for authentication. A JWT is a string with three parts: header, payload, and signature. When a user logs in, the server generates a token and sends it to the client. This token is then included in each request to validate the user’s identity.

Structure of a JWT

A JWT looks like this:

header.payload.signature

Prerequisites

To follow along, you’ll need:

  1. Node.js and npm installed.
  2. MongoDB and Mongoose for managing user data.
  3. Basic knowledge of Node.js, Express, and Mongoose.

Setting Up the Project

Let’s start by setting up a new Node.js project and installing the necessary dependencies.

Step 1: Initialize the Project

mkdir jwt-auth
cd jwt-auth
npm init -y

Step 2: Install Dependencies

Install Express, Mongoose, jsonwebtoken, and bcryptjs.

npm install express mongoose jsonwebtoken bcryptjs dotenv

Step 3: Set Up Environment Variables

Create a .env file in the project root with your MongoDB URI and JWT secret.

MONGODB_URI=mongodb://localhost:27017/jwt_auth
JWT_SECRET=your_jwt_secret
PORT=5000

Defining the User Model

Create a models folder and define a User model with Mongoose. The model includes fields for username, email, and hashed password.

models/User.js

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
 
const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true }
});
 
// Hash password before saving user
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});
 
// Compare password for login
userSchema.methods.comparePassword = async function (password) {
  return bcrypt.compare(password, this.password);
};
 
module.exports = mongoose.model("User", userSchema);

This schema includes:


Setting Up JWT Authentication

Let’s create functions for registering users, logging in, and generating a JWT.

Step 1: Setting Up Express and Connecting to MongoDB

Create a server.js file and initialize the Express app, configure middleware, and connect to MongoDB.

server.js

require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
 
const app = express();
app.use(express.json());
 
// Connect to MongoDB
mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
}).then(() => console.log("Connected to MongoDB"))
  .catch((error) => console.error("MongoDB connection error:", error));
 
// Routes
app.use("/auth", require("./routes/auth"));
 
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Step 2: Creating the Auth Routes

In the routes folder, create an auth.js file for handling authentication routes: register and login.

routes/auth.js

const express = require("express");
const jwt = require("jsonwebtoken");
const User = require("../models/User");
 
const router = express.Router();
 
// Helper function to generate JWT
const generateToken = (user) => {
  return jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
 
// Register Route
router.post("/register", async (req, res) => {
  try {
    const { username, email, password } = req.body;
    const existingUser = await User.findOne({ email });
    if (existingUser) return res.status(400).json({ message: "Email already registered" });
 
    const user = new User({ username, email, password });
    await user.save();
 
    const token = generateToken(user);
    res.status(201).json({ user, token });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
// Login Route
router.post("/login", async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ email });
    if (!user) return res.status(404).json({ message: "User not found" });
 
    const isPasswordValid = await user.comparePassword(password);
    if (!isPasswordValid) return res.status(400).json({ message: "Invalid credentials" });
 
    const token = generateToken(user);
    res.status(200).json({ user, token });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
module.exports = router;

In this code:

Token Generation

The generateToken function uses jwt.sign to create a token with a payload containing the user’s id and a 1-hour expiration.


Protecting Routes with JWT Middleware

Now, let’s create middleware to protect routes by verifying the JWT token.

Step 1: Creating the Authentication Middleware

In the middleware folder, create authMiddleware.js to check if the user is authenticated.

middleware/authMiddleware.js

const jwt = require("jsonwebtoken");
 
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ message: "Access denied. No token provided." });
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach the decoded user id to request
    next();
  } catch (error) {
    res.status(400).json({ message: "Invalid token." });
  }
};
 
module.exports = authMiddleware;

This middleware:

  1. Extracts the token from the Authorization header.
  2. Verifies the token using the JWT_SECRET.
  3. Attaches the decoded user ID to req.user if the token is valid.

Step 2: Protecting a Route

You can now use authMiddleware to protect any route. For example, create a profile route that only authenticated users can access.

routes/profile.js

const express = require("express");
const authMiddleware = require("../middleware/authMiddleware");
const User = require("../models/User");
 
const router = express.Router();
 
router.get("/profile", authMiddleware, async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select("-password"); // Exclude password
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
module.exports = router;

In server.js, add the profile route:

app.use("/profile", require("./routes/profile"));

Now, only authenticated users with a valid token can access the /profile route.


Testing the API

Use a tool like Postman to test your API:

  1. Register a new user by sending a POST request to /auth/register.

  2. Login with the user credentials by sending a POST request to /auth/login and get a token.

  3. Access the Profile Route by sending a GET request to /profile with the token in the Authorization header:

    Authorization: Bearer <token>

Best Practices

for JWT Authentication

  1. Use HTTPS: Always use HTTPS to prevent token interception.
  2. Set Expiration: Set token expiration to reduce the risk of token misuse if it’s compromised.
  3. Store Tokens Securely: Store tokens in secure storage (e.g., HTTP-only cookies or secure client storage).
  4. Implement Refresh Tokens: Use refresh tokens for session management and renew tokens upon expiration.
  5. Validate Token Expiry: Handle expired tokens gracefully on the client side to prompt users to log in again.

Conclusion

Implementing JWT authentication in a Node.js application with Mongoose provides a secure, stateless approach to managing user sessions. By creating a token during login and attaching it to requests, you can authenticate users and protect routes with ease.

With JWT authentication, your application is scalable, secure, and able to handle authenticated requests without relying on server-side sessions. Integrate these techniques into your project to enhance your application’s security and improve the user experience with seamless authentication.