🔐Authentication and Authorization – Securing Your Application

S.ArumugaselvamS.Arumugaselvam
7 min read

In today's digital world, authentication and authorization are two fundamental concepts every developer must understand to keep their web applications secure.

Whether you're building a simple blog or a full-scale SaaS product, protecting user data and restricting access to specific features is a must.

Before jumping into code, it’s important to clear up a common confusion — many developers think authentication and authorization are the same. They’re closely related but serve very different purposes.

✔Authentication : “Who are you?”

Authentication is the process of verifying the identity of a user.
It checks whether the person is genuine — for example, when a user logs in using their email and password.

Think of it like this:

🪪 You show your ID at the entrance to prove you are who you claim to be.

If the credentials match, the system knows this person is valid.

Examples:

  • Entering a username and password

  • Logging in with Google or GitHub

✔Authorization: "What are you allowed to do?"

Authorization happens after authentication. Once the system knows who you are, it decides what you're allowed to do — this is authorization.

It controls permissions and access levels.

Think of it like this:

After checking your ID, the security guard checks your pass to see which areas you're allowed to enter.

Examples:

  • Only admins can delete users

  • A normal user can view their profile but not someone else’s

Okay, now let’s dive deep into coding,

❇ Step 1: Project Setup

Before writing any code, let’s set up the project folder with the necessary dependencies.

Folder Structure

Here’s the simple folder structure we’ll follow:

mini-auth-system/
├── controllers/
│   └── authController.js
├── models/
│   └── user.js
├── routes/
│   └── authRoutes.js
├── middleware/
│   └── authMiddleware.js
├── .env
├── server.js
├── app.js
├── package.json

Required Packages

We will use the following packages:

  • express: Web framework

  • mongoose: MongoDB ODM

  • bcryptjs: For hashing passwords

  • jsonwebtoken: For token-based authentication

  • dotenv: To manage environment variables

  • cors : To prevent cors policy error.

  • cookie-parser : Middleware to read cookies in Express.

Install the required packages :

npm install express mongoose bcryptjs jsonwebtoken dotenv cors cookie-parser

Add Environment Variables in .env

Create a .env file and add:

PORT=5000
MONGO_URI=mongodb://localhost:27017/authdemo
JWT_SECRET=supersecretkey

Note: You can replace MONGO_URI with your own MongoDB Atlas URL if you’re using a cloud database.

🚀 Start the Server (server.js)

// server.js
import app from "./app.js";
import mongoose from "mongoose";
require("dotenv").config();

mongoose
  .connect(process.env.MONGO_URI)
  .then(() => {
    console.log("Connected to MongoDB");
    app.listen(process.env.PORT, () => {
      console.log(`Server running on port ${process.env.PORT}`);
    });
  })
  .catch((err) => console.error("MongoDB connection failed:", err));

🧱 Basic Express Setup (app.js)

// app.js
import express from "express";
import cors from "cors";

import authRoutes from "./routes/authRoutes.js";

const app = express();

app.use(cors({
  origin: "http://localhost:3000", //frontend origin (replace with your frontend url)
  credentials: true // allow sending cookies
}));
app.use(express.json());
app.use(cookieParser());

app.use("/api/auth", authRoutes);

export default app

✅ That’s it for the setup! We now have our basic structure in place.

Next, we’ll create the User model.

❇ Step 2: Create the User Model

We’ll use Mongoose to define our User schema. The user needs to have:

  • name: for displaying on profile

  • email: unique, used for login

  • password: stored in a hashed format

Create the User Model

Create a file: models/User.js

// models/User.js
import mongoose from "mongoose";
import bcrypt from "bcryptjs";

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, "Name is required"]
  },
  email: {
    type: String,
    required: [true, "Email is required"],
    unique: true
  },
  password: {
    type: String,
    required: [true, "Password is required"],
    minlength: 6
  }
});

// Hash the password before saving
userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Compare password method
userSchema.methods.comparePassword = function (enteredPassword) {
  return bcrypt.compare(enteredPassword, this.password);
};

const User = mongoose.model("User", userSchema);

export default User;

Quick Explanation:

  • bcrypt.genSalt(10): Generates a salt with 10 rounds (you can increase it for more security).

  • this.password = await bcrypt.hash(...): Replaces the plain password with a hashed one.

  • comparePassword(): A custom method we’ll use during login to validate user input.

  • isModified(field) is a method that checks whether a particular field (like "password") has been modified since it was loaded from the database or created.

❇ Step 3: Authentication Controller – Register & Login

We’ll handle two main operations here:

  1. Register → Save new users securely (with hashed password).

  2. Login → Validate credentials and return a JWT token.

Create controllers/authController.js

import User from "../models/User.js"; // Import the user model
import jwt from "jsonwebtoken"; // Import the jsonwebtoken package

// Generate a JWT using the JWT_SECRET value from the .env file
const generateToken = (userId) => {
  return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
    expiresIn: "1d"
  });
};

const cookieOptions = {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "strict",
  maxAge: 24 * 60 * 60 * 1000 // 1 day
};

// Register a new user
export const registerUser = async (req, res) => {
  try {
    // Extract user details from the request
    const { name, email, password } = req.body; 

    // Check if the user already exists
    const userExists = await User.findOne({ email });

   // If a user with the given email exists, return an error
    if (userExists) {
      return res.status(400).json({ message: "User already exists" });
    }

    // Create a new user if no existing user is found
    const newUser = await User.create({ name, email, password });

     // Generate a token for the new user
    const token = generateToken(newUser._id);

     // Storing cookies into cookie tab in the browser
    res.cookie("token", token, cookieOptions);
     // Send the response with user details
    res.status(201).json({
      message: "User registered successfully",
      user: {
        id: newUser._id,
        name: newUser.name,
        email: newUser.email
      }
    });
  } catch (error) {
    res.status(500).json({ message: "Registration failed", error: error.message });
  }
};

// Login user
export const loginUser = async (req, res) => {
  try {
    // Extract login credentials
    const { email, password } = req.body; 

   // Check if the user exists in the database
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ message: "Invalid email or password" });
    }

    // Check if the entered password matches the stored hashed password
    const isMatch = await user.comparePassword(password);

    // If the password doesn't match, return an error
    if (!isMatch) {
      return res.status(401).json({ message: "Invalid email or password" });
    }

    // Generate a token if login is successful
    const token = generateToken(user._id);

    // Storing cookies into cookie tab in the browser
    res.cookie("token", token, cookieOptions);

    // Send the response with user details 
    res.status(200).json({
      message: "Login successful",
      user: {
        id: user._id,
        name: user.name,
        email: user.email
      }
    });
  } catch (error) {
    res.status(500).json({ message: "Login failed", error: error.message });
  }
};

Step 4: Protecting Routes with Middleware

What is a Protected Route?

A protected route is a part of your backend API that should only be accessed by logged-in users. For example:

  • A user’s dashboard, Admin-only areas, Updating a profile

To do this, we check if the request has a valid JWT token before allowing access.

How it Works

  1. The user logs in and receives a JWT token.

  2. For any protected route, the frontend must send this token in the Authorization header.

  3. The backend checks the token:

    • If valid → allow access || If missing or invalid → reject access

Create middleware/authMiddleware.js

// middleware/authMiddleware.js
import jwt from "jsonwebtoken";
import User from "../models/User.js";

export const protect = async (req, res, next) => {
  const token = req.cookies.token;
 if (!token) {
    return res.status(401).json({ message: "No token, access denied" });
  }
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.id).select("-password");
    next();
  } catch (err) {
    return res.status(401).json({ message: "Invalid token" });
  }
};

Step 5: Create Authentication Routes

Create routes/authRoutes.js

// routes/authRoutes.js
import express from "express";
const router = express.Router();
import { registerUser, loginUser } from "../controllers/authController.js";
import protect from "../middleware/authMiddleware.js";

// Public routes
router.post("/register", registerUser);
router.post("/login", loginUser);

// Protected route example
router.get("/profile", protect, (req, res) => {
  res.status(200).json({
    message: "Access granted to protected route",
    user: req.user
  });
});

export default router;

By now, you’ve successfully built a fully functional authentication system in Node.js with:

🔒 Password hashing using bcrypt
🔑 JWT-based login, securely stored as an HTTP-only cookie
🔐 Protected routes using custom middleware
🍪 Token storage via res.cookie(), making your app more secure against XSS attacks

This gives you a solid foundation to build more advanced features like:

  • ✉️ Email verification

  • 👮‍♂️ Role-based access control

  • ⚛️ Frontend integration (React, Next.js, etc.)


💡 Developer Challenge:

If you’re reading this, here’s your next task:
Try writing the logout controller yourself!
It should clear the JWT cookie and return a success message.
Take this as an opportunity to practice, research, and level up 🚀

1
Subscribe to my newsletter

Read articles from S.Arumugaselvam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

S.Arumugaselvam
S.Arumugaselvam