Implementing Authentication in a RESTful API with Node.js, Express, and JWT

November 2, 2024 (2w ago)

Implementing Authentication in a RESTful API with Node.js, Express, and JWT

Authentication is a fundamental aspect of secure API development, allowing you to protect routes and control access to resources. By implementing JWT (JSON Web Tokens) for authentication in a Node.js and Express application, you can achieve stateless and secure access for your users. This guide covers setting up user registration, logging in, generating JWT tokens, and securing routes with JWT-based authentication.


What is JWT and Why Use It?

JWT (JSON Web Token) is a compact, URL-safe token that is commonly used to securely transmit information between parties. It’s ideal for stateless authentication because it doesn’t require server-side sessions, making it scalable and efficient for RESTful APIs.

JWT Structure

A JWT consists of three parts:

  1. Header: Contains the algorithm and token type (usually "HS256" for HMAC SHA256).
  2. Payload: Contains claims, which are statements about the user (e.g., user ID).
  3. Signature: A hash of the header, payload, and a secret key that ensures token integrity.

Setting Up the Project

If you’re starting fresh, initialize a new project and install the required dependencies.

Step 1: Initialize the Project and Install Dependencies

mkdir jwt-auth-api
cd jwt-auth-api
npm init -y
npm install express mongoose bcryptjs jsonwebtoken dotenv

Step 2: Configure Environment Variables

Create a .env file to store sensitive information, including the MongoDB URI and JWT secret.

.env

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

Setting Up User Registration and Login

To authenticate users, we need to set up user registration and login routes. These routes will handle creating users and issuing JWT tokens.

Step 1: Create the User Model

Create a models folder with a User.js file to define the user schema.

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 the password before saving
userSchema.pre("save", async function (next) {
  if (this.isModified("password")) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});
 
// Password comparison method
userSchema.methods.comparePassword = async function (password) {
  return bcrypt.compare(password, this.password);
};
 
module.exports = mongoose.model("User", userSchema);

In this schema:

Step 2: Implement Registration and Login Routes

Create an auth.js route file to handle user registration and login.

routes/auth.js

const express = require("express");
const User = require("../models/User");
const jwt = require("jsonwebtoken");
 
const router = express.Router();
 
// User registration
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 in use" });
 
    const user = new User({ username, email, password });
    await user.save();
    res.status(201).json({ message: "User registered successfully" });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
// User login
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 = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1h" });
    res.status(200).json({ message: "Login successful", token });
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
});
 
module.exports = router;

In this code:


Securing Routes with JWT Authentication

Now that users can log in and receive tokens, let’s create middleware to protect specific routes by verifying the JWT.

Step 1: Create Authentication Middleware

Create a middleware folder with an authMiddleware.js file to verify JWT tokens.

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;
    next();
  } catch (error) {
    res.status(403).json({ message: "Invalid or expired token" });
  }
};
 
module.exports = authMiddleware;

In this middleware:

Step 2: Protect Routes with the Middleware

Let’s create a protected route that requires authentication. Create a profile.js route file and protect it with authMiddleware.

routes/profile.js

const express = require("express");
const authMiddleware = require("../middleware/authMiddleware");
 
const router = express.Router();
 
// Protected route
router.get("/", authMiddleware, (req, res) => {
  res.status(200).json({ message: `Welcome, user ${req.user.id}!` });
});
 
module.exports = router;

In this route:


Integrating Routes with Express

In server.js, add the auth and profile routes to the main Express server.

server.js

require("dotenv").config();
const express = require("express");
const mongoose = require("mongoose");
 
const authRoutes = require("./routes/auth");
const profileRoutes = require("./routes/profile");
 
const app = express();
const port = process.env.PORT || 5000;
 
app.use(express.json()); // Middleware to parse JSON
 
// MongoDB connection
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("/api/auth", authRoutes);
app.use("/api/profile", profileRoutes); // Protected route
 
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

Testing the API with Postman

  1. Register a User: Send a POST request to http://localhost:5000/api/auth/register with JSON data:

    {
      "username": "testuser",
      "email": "testuser@example.com",
      "password": "password123"
    }
  2. Log In: Send a POST request to http://localhost:5000/api/auth/login with email and password. The response will include a JWT token.

  3. Access Protected Route: Send a GET request to http://localhost:5000/api/profile with the token in the Authorization header as Bearer <token>. If the token is valid, you’ll receive a welcome message.


Best Practices for Securing JWT Authentication

  1. Set Short Expiration Times: Use short expiration times for tokens (e.g., 1 hour) to limit the impact of a compromised token.
  2. Use HTTPS: Always use HTTPS in production to prevent tokens from being intercepted.
  3. Store Tokens Securely: Store tokens in a secure location, such as HTTP-only cookies or secure client storage.
  4. Refresh Tokens: Implement refresh tokens to renew access tokens without requiring the user to log in again.
  5. Rotate Secrets: Regularly update your JWT secret key for enhanced security.

Conclusion

Implementing authentication with JWT in a RESTful API allows you to protect

routes and ensure only authorized users can access specific resources. By setting up registration and login routes, generating tokens, and using middleware for token verification, you can secure your Node.js API effectively.

Integrate these techniques into your API to provide secure, scalable, and user-friendly authentication, enhancing both security and user experience.