DEV Community

Yash Matrani
Yash Matrani

Posted on

# The MERN Auth Flow: How JWT actually works (Without the headache)

Authentication is the most confusing part of learning the MERN stack.

When I started building full-stack apps, I struggled to understand what actually happens between clicking "Login" and seeing the dashboard. I read tutorials that used NextAuth, Firebase, or Passport.js, but I never understood the core mechanism.

So I decided to build it from scratch. Here is the exact flow of JWT (JSON Web Token) authentication in a MERN app.

The 3-Step Architecture

Authentication is basically a bouncer at a club.

  1. Verify: Check the ID (Email + Password).
  2. Issue: Give a wristband (JWT Token).
  3. Check: Verify the wristband on every request.

Here is how it translates to code.

Step 1: Hashing the Password (Backend)

Never store passwords as plain text. When a user registers, we use bcrypt to hash the password before saving it to MongoDB.


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

// During Registration
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(req.body.password, salt);

const newUser = new User({
  email: req.body.email,
  password: hashedPassword
});
await newUser.save();
Step 2: Generating the JWT (Backend)
When the user logs in, we compare the entered password with the hashed password. If it matches, we generate a JWT using the jsonwebtoken library.

javascript
import jwt from "jsonwebtoken";

const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(404).json("User not found");

const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).json("Wrong password");

// Create Token
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1d" });
res.json({ token, user });
Step 3: Storing the Token (Frontend)
On the React frontend, we receive this token and store it in localStorage.

javascript
const res = await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
const { token } = await res.json();

// Save the token
localStorage.setItem("token", token);
window.location.href = "/dashboard"; // Redirect
Step 4: Protecting Routes with Middleware
Now, whenever the user visits a protected route (like /dashboard), the frontend must send the token. But the backend needs to verify it. We do this using Middleware.

javascript
import jwt from "jsonwebtoken";

export const verifyToken = (req, res, next) => {
  const authHeader = req.headers.token;

  if (authHeader) {
    jwt.verify(authHeader, process.env.JWT_SECRET, (err, user) => {
      if (err) return res.status(403).json("Token is not valid!");
      req.user = user;
      next(); // Proceed to the actual route
    });
  } else {
    return res.status(401).json("You are not authenticated!");
  }
};
Step 5: Using the Middleware
javascript
app.get("/api/dashboard", verifyToken, async (req, res) => {
  // req.user now contains the decoded ID from the token
  const user = await User.findById(req.user.id);
  res.json(user);
});
Why this matters
Tools like NextAuth or Clerk are great, but if you don't understand the underlying JWT mechanism, you will struggle when you need custom logic (like Role-Based Access Control for an Admin Panel).

Building it from scratch takes 2 days, but it gives you 10 years of clarity.

I'm 16 and currently building full-stack projects for real clients. If you are learning MERN, keep pushing.

Follow my journey on X: @Developervxku
Enter fullscreen mode Exit fullscreen mode

Top comments (0)