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)

  1. Installed the required dependencies:
npm install jsonwebtoken bcryptjs dotenv
  1. 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" });
   };
  1. 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

  1. Generated a refresh token along with the access token:
const refreshToken = jwt.sign({ id: user._id }, process.env.REFRESH_SECRET, { expiresIn: "7d" });
  1. Stored the refresh token securely (e.g., in an HTTP-only cookie).
  2. 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

  1. 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" }
   });
  1. 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();
   };
  1. 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

  1. Installed the CORS package:
npm install cors
  1. Configured CORS in Express.js:
const cors = require("cors");
   app.use(cors({
       origin: "http://localhost:3000", // React frontend
       credentials: true,
   }));
  1. 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. 😊