🔐Authentication and Authorization – Securing Your Application

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 frameworkmongoose
: MongoDB ODMbcryptjs
: For hashing passwordsjsonwebtoken
: For token-based authenticationdotenv
: To manage environment variablescors
: 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 profileemail
: unique, used for loginpassword
: 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:
Register → Save new users securely (with hashed password).
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
The user logs in and receives a JWT token.
For any protected route, the frontend must send this token in the Authorization header.
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;
🏁 Conclusion: You Just Built a Mini Auth System with Cookie-Based JWT!
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 thelogout
controller yourself!
It should clear the JWT cookie and return a success message.
Take this as an opportunity to practice, research, and level up 🚀
Subscribe to my newsletter
Read articles from S.Arumugaselvam directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
