Le Do Nghiem

Le Do Nghiem

AI Engineer

About meBooksSnippetsBlog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Blog

JWT vs Session Authentication: Choosing the Right Approach

Le Do Nghiem
Le Do NghiemAI Engineer
2025-12-17 4 min read
Share

Sessions vs JWT — the tradeoff in plain terms

JWT vs sessions is not a religion. It is a tradeoff about where you store trust — server memory vs signed client payload — and how fast you need to revoke access.

I have shipped both. I have regretted neither when the choice matched the architecture. I have regretted picking based on a Medium headline.

For cookie and Server Component nuances in Next.js, pair this with use client vs use server.


My decision framework (30 seconds)

SituationWhat I lean toward
Traditional web app, same domain, need logout nowSessions (httpOnly cookie)
Mobile app + API, multiple servicesJWT (+ refresh token)
SPA on separate domain from APIJWT or session with careful CORS/cookie setup
Banking / healthcare / immediate revokeSessions or very short JWT + server-side denylist
Microservices, no shared session storeJWT with shared signing keys

Still fuzzy? Default to sessions in httpOnly cookies for a monolith web app. Default to short access JWT + refresh for API-first or mobile.


Session-based authentication

What: Server stores session; client gets a session ID (usually in a cookie).

app.post("/login", async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).json({ error: "Invalid credentials" });
  }
  req.session.userId = user.id;
  req.session.save();
  res.json({ message: "Login successful" });
});

app.get("/profile", requireAuth, (req, res) => {
  res.json({ user: req.user });
});

Why I like it:

  • Revoke instantly — delete session server-side
  • Sensitive claims stay on server
  • Logout is real

Costs:

  • Redis/DB for sessions at scale
  • Cross-domain SPA setup is fiddly

Red flags:

  • Session cookie without httpOnly, secure, sameSite
  • No HTTPS in production
  • Session fixation — regenerate session ID on login

JWT-based authentication

What: Signed token on the client; server verifies signature each request.

import jwt from "jsonwebtoken";

app.post("/login", async (req, res) => {
  const user = await validateUser(req.body);
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: "7d" }
  );
  res.json({ token });
});

function authenticateToken(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ error: "No token" });
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: "Invalid token" });
    req.user = user;
    next();
  });
}

Why I like it:

  • Stateless API tier — no session lookup on every service
  • Mobile-friendly — Authorization header
  • Cross-domain APIs

Costs:

  • Hard to revoke without extra machinery
  • Token size vs session ID
  • Stolen token valid until expiry

Red flags:

  • JWT in localStorage — XSS steals it; prefer httpOnly cookie or secure mobile storage
  • Huge payloads — JWT is not a database
  • No expiry — always set exp
  • Weak secrets — use long random keys, rotate with plan

Hybrid: refresh tokens

What: Short access token (15m) + long refresh token stored server-side.

app.post("/login", async (req, res) => {
  const user = await validateUser(req.body);
  const accessToken = jwt.sign({ userId: user.id }, process.env.ACCESS_TOKEN_SECRET, {
    expiresIn: "15m",
  });
  const refreshToken = jwt.sign({ userId: user.id }, process.env.REFRESH_TOKEN_SECRET, {
    expiresIn: "7d",
  });
  await RefreshToken.create({ userId: user.id, token: refreshToken });
  res.json({ accessToken, refreshToken });
});

Why: Balance stateless APIs with revocable refresh tokens.

Gotcha: Store refresh tokens hashed; rotate on use; detect reuse.


Security practices I do not skip

Sessions:

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      secure: process.env.NODE_ENV === "production",
      httpOnly: true,
      maxAge: 24 * 60 * 60 * 1000,
      sameSite: "strict",
    },
  })
);

JWT:

jwt.sign(payload, process.env.JWT_SECRET, {
  expiresIn: "15m",
  issuer: "your-app",
  audience: "your-app-users",
});

Auth mistakes I shipped

  • JWT in localStorage because it was "easier than cookies" — one XSS away from account takeover.
  • Same secret for access and refresh — separate keys, separate lifetimes.
  • Logout that only deleted client token — JWT logout needs blocklist or short TTL + refresh revoke.

Defaults I'd pick today

Name your app shape: monolith web, SPA + API, mobile, microservices. Pick session, JWT, or hybrid from the table — not from Twitter.

Auth method is half the story. Implementation is the other half: HTTPS, httpOnly cookies, short TTLs, rotation, and a plan for stolen tokens.

Then read how auth cookies interact with Server Components in use client vs use server.

On this page

  • Sessions vs JWT — the tradeoff in plain terms
  • My decision framework (30 seconds)
  • Session-based authentication
  • JWT-based authentication
  • Hybrid: refresh tokens
  • Security practices I do not skip
  • Auth mistakes I shipped
  • Defaults I'd pick today
Share
Previous Post

Dependency Injection in ASP.NET Core

Next Post

Understanding 'use client' and 'use server' in Next.js