Introduction
Hey everyone! If you've ever worked on a full-stack project, you know that authentication and authorization can be a real headache. When I first started working with the MERN stack (MongoDB, Express.js, React.js, Node.js), I struggled with setting up a solid authentication system. Debugging token issues, handling user sessions, and dealing with CORS errors were some of the most frustrating parts. But after digging deep and trying out different approaches, I finally figured out how to do it the right way.
In this blog, I'll share my experience building a secure authentication system using JWT (JSON Web Tokens), Refresh Tokens, and Role-Based Access Control (RBAC). I'll also cover CORS (Cross-Origin Resource Sharing) and how to configure it properly. Let's get started! 🚀
1. Authentication vs. Authorization (The Confusion I Had)
When I started, I used to mix up authentication and authorization. Here’s the difference:
- Authentication: Verifying who the user is (e.g., logging in with an email and password).
- Authorization: Deciding what the authenticated user can do (e.g., admin vs. regular user access).
Once I got this straight, things became a lot easier.
2. Implementing JWT-Based Authentication
My Experience with JWT
JWT (JSON Web Token) is a widely used way to handle authentication in web apps. At first, I found it confusing, but once I understood its structure, it made perfect sense. A JWT consists of:
- Header (Algorithm & Token Type)
- Payload (User Information & Claims)
- Signature (Verifies authenticity)
How I Implemented JWT Authentication in MERN
Backend (Node.js + Express)
- Installed the required dependencies:
npm install jsonwebtoken bcryptjs dotenv
- Wrote a function to generate JWT tokens:
const jwt = require("jsonwebtoken");
const generateToken = (userId) => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
- Created middleware to protect routes:
const authMiddleware = (req, res, next) => {
const token = req.header("Authorization").replace("Bearer ", "");
if (!token) return res.status(401).json({ message: "No token, authorization denied" });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: "Invalid token" });
}
};
Frontend (React.js)
- I stored the JWT token securely in HTTP-only cookies instead of localStorage for better security.
- Every protected API request included the token in the
Authorization
header.
3. Refresh Tokens (Fixing Expired Sessions)
I noticed that JWTs expire quickly (for security reasons), which frustrated users since they had to log in frequently. The solution? Refresh Tokens.
How I Implemented Refresh Tokens
- Generated a refresh token along with the access token:
const refreshToken = jwt.sign({ id: user._id }, process.env.REFRESH_SECRET, { expiresIn: "7d" });
- Stored the refresh token securely (e.g., in an HTTP-only cookie).
- Created an endpoint to refresh the JWT token:
app.post("/refresh-token", (req, res) => {
const { refreshToken } = req.body;
jwt.verify(refreshToken, process.env.REFRESH_SECRET, (err, user) => {
if (err) return res.status(403).json({ message: "Invalid refresh token" });
const newToken = generateToken(user.id);
res.json({ token: newToken });
});
});
4. Role-Based Access Control (RBAC) - Making My App Secure
Why I Needed RBAC
I wanted admin users to have extra privileges while regular users had limited access. That’s where RBAC (Role-Based Access Control) helped me.
How I Implemented RBAC
- Added a role field to the User model:
const UserSchema = new mongoose.Schema({
username: String,
email: String,
password: String,
role: { type: String, enum: ["admin", "user"], default: "user" }
});
- Created middleware to protect admin routes:
const roleMiddleware = (roles) => (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: "Access denied" });
}
next();
};
- Applied it to admin routes:
app.get("/admin-dashboard", authMiddleware, roleMiddleware(["admin"]), (req, res) => {
res.json({ message: "Welcome to the admin dashboard" });
});
5. CORS (Fixing Annoying Frontend Errors)
My Issue with CORS
At one point, my frontend (React) was unable to call my backend (Node.js) due to CORS errors. I fixed it by properly configuring CORS in my Express server.
Fixing CORS in Express.js
- Installed the CORS package:
npm install cors
- Configured CORS in Express.js:
const cors = require("cors");
app.use(cors({
origin: "http://localhost:3000", // React frontend
credentials: true,
}));
- Sent requests from React using:
axios.get("http://localhost:5000/api", { withCredentials: true });
Conclusion
This journey of implementing JWT, refresh tokens, RBAC, and CORS in my MERN app taught me a lot. Debugging authentication issues was frustrating at first, but solving them helped me gain a deep understanding of how security works in full-stack applications. If you're working on authentication, I hope this guide helps you avoid the struggles I faced! 🚀
Let me know if you've faced similar issues or have any better approaches! Drop your thoughts in the comments. 😊