Cookie Session Management
Last Updated : Oct 2026
Storing JWTs in httpOnly cookies protects them from Cross-Site Scripting (XSS) attacks because client-side JavaScript cannot access them. Using a short-lived Access Token alongside a long-lived Refresh Token maximizes security: even if an Access Token is intercepted, its brief lifespan minimizes damage.
For Understanding, we make a simple Authentication System.Where a user can register, login and logout.
So let's get started :
Installing Dependencies
npm install express jsonwebtoken cookie-parser dotenv
.env File
PORT=3000
ACCESS_TOKEN_SECRET=your_access_token_super_secret_key_123
REFRESH_TOKEN_SECRET=your_refresh_token_super_secret_key_456
Token Generation utils/jwt.js
const jwt = require("jsonwebtoken");
// Access Token: Short-lived (e.g., 15 minutes)
export const generateAccessToken = (user) => {
return jwt.sign(
{ id: user.id, role: user.role },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: "15m" },
);
};
// Refresh Token: Long-lived (e.g., 7 days)
export const generateRefreshToken = (user) => {
return jwt.sign({ id: user.id }, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: "7d",
});
};
Authentication & Cookie Routes (routes/auth.js)
import express from "express";
import jwt from "jsonwebtoken";
import { generateAccessToken, generateRefreshToken } from "../lib/jwt.js";
const router = express.Router();
// Mock database and token whitelist/store
const usersDB = [
{
id: 1,
email: "tukkaLearn@example.com",
password: "hashed_password",
role: "user",
},
];
let refreshTokensDB = []; // In production, store this in Redis or a Database!
// POST /api/auth/login
router.post("/login", (req, res) => {
const { email, password } = req.body;
const user = usersDB.find((u) => u.email === email);
if (!user) return res.status(401).json({ error: "Invalid credentials" });
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Save refresh token in database (allows revocation)
refreshTokensDB.push(refreshToken);
// Set Refresh Token in an httpOnly Cookie
res.cookie("refreshToken", refreshToken, {
httpOnly: true, // Prevents XSS reading the cookie via JS
secure: process.env.NODE_NODE_ENV === "production", // HTTPS only in prod
sameSite: "strict", // Prevents CSRF attacks
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days in milliseconds
});
// Return Access Token in JSON body (stored in memory on frontend)
res.json({ accessToken });
});
// POST /api/auth/refresh (Obtain new Access Token using Cookie)
router.post("/refresh", (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: "Refresh token missing" });
}
// Check if token exists in database whitelist
if (!refreshTokensDB.includes(refreshToken)) {
return res.status(403).json({ error: "Invalid or revoked refresh token" });
}
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, decoded) => {
if (err)
return res
.status(403)
.json({ error: "Expired or invalid refresh token" });
const user = usersDB.find((u) => u.id === decoded.id);
if (!user) return res.status(404).json({ error: "User not found" });
// Issue a new Access Token
const newAccessToken = generateAccessToken(user);
res.json({ accessToken: newAccessToken });
});
});
// POST /api/auth/logout
router.post("/logout", (req, res) => {
const refreshToken = req.cookies.refreshToken;
// Remove from database store
refreshTokensDB = refreshTokensDB.filter((token) => token !== refreshToken);
// Clear cookie
res.clearCookie("refreshToken", {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
});
res.json({ message: "Logged out successfully" });
});
export default router;
Server
import express from "express";
import cookieParser from "cookie-parser";
import dotenv from "dotenv";
import authRoutes from "./routes/auth.js";
dotenv.config();
const app = express();
app.use(express.json());
app.use(cookieParser()); // Parses incoming cookies into req.cookies
app.use("/api/auth", authRoutes);
app.listen(3000, () => console.log("Server running on port 3000"));
