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
-
User logs in → server issues:
-
Access token (short-lived)
-
Refresh token (long-lived, stored securely)
-
-
Client stores:
-
Access token → memory (or in-browser memory)
-
Refresh token → HTTP-only cookie or secure storage
-
-
When access token expires → client sends refresh token to
/refresh
endpoint -
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
-
HTTP-only cookies for refresh tokens (prevents XSS)
-
Short-lived access tokens + long-lived refresh tokens
-
Store refresh tokens server-side if possible
-
Rotate refresh tokens on every use
-
Revoke refresh tokens on logout or password change
-
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