Siksha Sarovar

Siksha Sarovar (sikshasarovar.com) is a free educational web application that helps students in India learn programming and prepare for academic and competitive exams. The platform offers structured coding courses (C, C++, Python, Java, HTML, CSS, PHP, Power BI, AI, Machine Learning, Data Science), complete university curriculum notes for BCA/MCA students with previous year question papers, Class 10 and Class 12 CBSE/HBSE school notes, and dedicated preparation material for SSC, UPSC, Banking, Railway and other government exams. Browsing the site is completely free and requires no account. Users may optionally sign in with Google solely to save their learning progress, quiz scores and personal preferences across devices.

Privacy Policy | Terms of Service | Contact Siksha Sarovar | About Siksha Sarovar

v4.0.9 · PWA
Siksha Sarovar logo
Siksha Sarovar
Your Learning Universe

Siksha Sarovar is a free e-learning platform for coding courses, BCA university notes and competitive exam preparation. Optional Google sign-in saves your learning progress across devices.

Initializing knowledge base…
Compiling modules 0%

15. Access Token and Refresh Token — Deep Dive

Lesson 15 of 23 in the free Backend Development notes on Siksha Sarovar, written by Rohit Jangra.

Why Two Tokens?

The two-token system balances security and usability:

  • A single long-lived token would be convenient, but if it is stolen, the attacker has access for a long time
  • A single short-lived token is secure, but the user would be logged out every 15 minutes and have to re-enter their password

Solution: Use a short-lived access token (15 min – 1 hour) for API requests, and a long-lived refresh token (7 – 30 days) stored securely for generating new access tokens silently.

---

Token Lifecycle

Login → Access Token (15m) + Refresh Token (7d) issued
                ↓
API requests with Access Token (until it expires)
                ↓
Access Token expires → Send Refresh Token → Get new Access Token
                ↓
Refresh Token expires → User must log in again

---

Login Controller — Step by Step

  1. Find user by email OR username in database
  2. Check if password is correct (isPasswordCorrect instance method)
  3. Generate new access token and refresh token
  4. Save the refresh token to the user document in MongoDB
  5. Set both tokens as httpOnly cookies
  6. Return sanitised user data (no password, no refreshToken)
const loginUser = asyncHandler(async (req, res) => {
  const { email, username, password } = req.body;

  if (!email && !username) throw new ApiError(400, 'Email or username is required');
  if (!password) throw new ApiError(400, 'Password is required');

  // Find user — select password field explicitly (it has select: false)
  const user = await User.findOne({
    $or: [{ email: email?.toLowerCase() }, { username: username?.toLowerCase() }],
  }).select('+password');

  if (!user) throw new ApiError(404, 'User not found');

  const isPasswordValid = await user.isPasswordCorrect(password);
  if (!isPasswordValid) throw new ApiError(401, 'Invalid credentials');

  // Generate tokens
  const accessToken = user.generateAccessToken();
  const refreshToken = user.generateRefreshToken();

  // Save refreshToken to DB for revocation capability
  user.refreshToken = refreshToken;
  await user.save({ validateBeforeSave: false });

  // Get clean user without sensitive fields
  const loggedInUser = await User.findById(user._id).select('-password -refreshToken');

  // Set cookies
  res
    .status(200)
    .cookie('accessToken', accessToken, { ...cookieOptions, maxAge: 15 * 60 * 1000 })
    .cookie('refreshToken', refreshToken, { ...cookieOptions, maxAge: 7 * 24 * 60 * 60 * 1000 })
    .json(new ApiResponse(200, { user: loggedInUser, accessToken }, 'Login successful'));
});

---

Refresh Access Token Controller

Called by the frontend when the access token expires:

  1. Read refresh token from cookie (or request body as fallback)
  2. Verify the refresh token with JWT_REFRESH_SECRET
  3. Find the user by the decoded _id (select refreshToken field)
  4. Compare the incoming refresh token with the one stored in DB (prevents reuse of revoked tokens)
  5. Generate new access token (and optionally a new refresh token — token rotation)
  6. Return new tokens
const refreshAccessToken = asyncHandler(async (req, res) => {
  const incomingRefreshToken = req.cookies?.refreshToken || req.body.refreshToken;
  if (!incomingRefreshToken) throw new ApiError(401, 'Refresh token not found');

  const decoded = jwt.verify(incomingRefreshToken, process.env.JWT_REFRESH_SECRET);
  const user = await User.findById(decoded._id).select('+refreshToken');
  if (!user) throw new ApiError(401, 'Invalid refresh token');

  // Security check: token must match what is stored in DB
  if (incomingRefreshToken !== user.refreshToken) {
    throw new ApiError(401, 'Refresh token is expired or has been used');
  }

  const accessToken = user.generateAccessToken();
  const newRefreshToken = user.generateRefreshToken();

  // Token rotation: update stored refresh token
  user.refreshToken = newRefreshToken;
  await user.save({ validateBeforeSave: false });

  res
    .status(200)
    .cookie('accessToken', accessToken, { ...cookieOptions, maxAge: 15 * 60 * 1000 })
    .cookie('refreshToken', newRefreshToken, { ...cookieOptions, maxAge: 7 * 24 * 60 * 60 * 1000 })
    .json(new ApiResponse(200, { accessToken }, 'Access token refreshed'));
});

---

Logout Controller

Clears cookies and removes refresh token from database:

const logoutUser = asyncHandler(async (req, res) => {
  // Remove refresh token from DB (revoke the token)
  await User.findByIdAndUpdate(
    req.user._id,
    { $unset: { refreshToken: 1 } },  // $unset removes the field
    { new: true }
  );

  // Clear both cookies
  res
    .status(200)
    .clearCookie('accessToken', cookieOptions)
    .clearCookie('refreshToken', cookieOptions)
    .json(new ApiResponse(200, {}, 'Logged out successfully'));
});

---

Token Storage Security

Storage LocationXSS AttackCSRF AttackRecommended
httpOnly Cookie✅ Safe❌ Vulnerable✅ With SameSite=strict
localStorage❌ Vulnerable✅ Safe❌ Never for auth tokens
sessionStorage❌ Vulnerable✅ Safe❌ Never for auth tokens
In-memory (JS variable)✅ Safe✅ Safe✅ For access token only

Best practice: Store access token in memory (JavaScript variable, Redux state) and refresh token in an httpOnly cookie.

---

validateBeforeSave: false

When updating only the refreshToken field on a user document, pass { validateBeforeSave: false } to user.save() to skip full validation (which would require all required fields to be present again). This is safe when you are only updating one field and not changing the user's core data.