Let’s go through refresh tokens and token rotation, which are essential for secure, long-lived sessions in JWT-based authentication.


1️⃣ Why Refresh Tokens?

  • Access tokens (JWT) are usually short-lived (e.g., 15–60 min) for security.

  • Refresh tokens are long-lived (e.g., days/weeks) and used to obtain new access tokens without forcing the user to log in again.

  • Helps mitigate token theft: even if an access token is stolen, it expires quickly.


2️⃣ Refresh Token Flow

  1. User logs in → server issues:

    • Access token (short-lived)

    • Refresh token (long-lived, stored securely)

  2. Client stores:

    • Access token → memory (or in-browser memory)

    • Refresh token → HTTP-only cookie or secure storage

  3. When access token expires → client sends refresh token to /refresh endpoint

  4. Server validates refresh token → issues new access token (and optionally new refresh token)


3️⃣ Storing Refresh Tokens

  • Server-side storage recommended (DB or Redis) for invalidation and rotation.

  • Example table (SQL) or collection (MongoDB):

userId | refreshToken | expiresAt
  • Optional: blacklist revoked tokens.

4️⃣ Node.js Implementation (JWT + Refresh Tokens)

Issue Tokens on Login

const jwt = require("jsonwebtoken");
 
const generateTokens = (user) => {
  const accessToken = jwt.sign(
    { userId: user._id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: "15m" }
  );
 
  const refreshToken = jwt.sign(
    { userId: user._id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: "7d" }
  );
 
  return { accessToken, refreshToken };
};

Refresh Endpoint

const refreshTokenHandler = async (req, res) => {
  const { token } = req.body;
  if (!token) return res.status(401).json({ error: "No token provided" });
 
  try {
    const decoded = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
 
    // Optional: check DB if token is valid (rotation)
    // const exists = await RefreshToken.findOne({ token });
    // if (!exists) return res.status(401).json({ error: "Invalid token" });
 
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.JWT_SECRET,
      { expiresIn: "15m" }
    );
 
    res.json({ accessToken: newAccessToken });
  } catch (err) {
    res.status(401).json({ error: "Invalid or expired refresh token" });
  }
};

Optional: Token Rotation

  • After refresh, invalidate old refresh token and issue a new one

  • Helps prevent reuse of stolen refresh tokens


5️⃣ Security Best Practices

  1. HTTP-only cookies for refresh tokens (prevents XSS)

  2. Short-lived access tokens + long-lived refresh tokens

  3. Store refresh tokens server-side if possible

  4. Rotate refresh tokens on every use

  5. Revoke refresh tokens on logout or password change

  6. Use separate secrets for access and refresh tokens


✅ Summary

  • Access token → short-lived, used for API calls

  • Refresh token → long-lived, used to request new access tokens

  • Rotation → replaces refresh token on every use for extra security

  • Works with JWT, route protection, OAuth, and bcrypt password hashing