Understanding Access and Refresh Tokens in Authentication using Node, Express, Typescript
Access Token
An Access Token is a short-lived token used to:
Authenticate users and grant access to protected routes and resources.
Extract user details for authorization purposes.
Ensure security by expiring after a short duration (e.g., 15 minutes).
Refresh Token
A Refresh Token is a long-lived token used to:
Generate a new access token when the existing one expires.
Maintain user sessions without requiring frequent logins.
Improve user experience by reducing unnecessary re-authentication.
How Access and Refresh Tokens Work
User Login: When a user logs in, both access and refresh tokens are generated with specific expiry times.
Token Storage:
The access token is stored in memory or local storage (for web) and secure storage (for mobile).
The refresh token is stored in an HTTP-only secure cookie (for web) or secure storage (for mobile).
Accessing Protected Routes: The access token is included in requests (usually in the
Authorization
header asBearer <token>
) to authenticate users.Token Expiry & Refresh: If the access token expires while the user is active, the refresh token is used to generate a new access token without requiring the user to log in again.
Why Use Both Access and Refresh Tokens?
Problem Without a Refresh Token:
If you only use an access token, when it expires, the user is logged out and must log in again. This can disrupt the user experience.
Solution with a Refresh Token:
By using a refresh token, the system can seamlessly generate a new access token without logging the user out, improving security and usability.
Token Strategy for Web and Mobile Applications
Platform | Access Token | Refresh Token |
Web | Memory / HTTP-only cookie | HTTP-only secure cookie |
Mobile | Secure Storage (Keychain for iOS, EncryptedSharedPreferences for Android) | Secure Storage |
Security Best Practices
Use HTTP-only cookies for refresh tokens to prevent XSS (Cross-Site Scripting) attacks.
Store access tokens in memory rather than local storage to reduce vulnerability to XSS attacks.
Implement token rotation to enhance security (generate a new refresh token upon usage).
Use short-lived access tokens (e.g., 15 minutes) and longer-lived refresh tokens (e.g., 7 days).
Revoke refresh tokens if the user logs out or changes credentials.
Code to Generate Access and Refresh Tokens
import jwt from "jsonwebtoken";
const generateAccessToken = (userId: string, role: string) => {
return jwt.sign({ userId, role }, process.env.ACCESS_TOKEN_SECRET!, {
expiresIn: "15m",
});
};
const generateRefreshToken = (userId: string, tokenVersion: number) => {
return jwt.sign({ userId, tokenVersion }, process.env.REFRESH_TOKEN_SECRET!, {
expiresIn: "7d",
});
};
login Controller
import { Request, Response } from "express";
import bcrypt from "bcryptjs";
import { generateAccessToken, generateRefreshToken } from "../utils/jwt";
import UserModel from "../models/User";
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
const user = await UserModel.findOne({ email });
if (!user) return res.status(400).json({ message: "User not found" });
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword)
return res.status(400).json({ message: "Invalid credentials" });
const accessToken = generateAccessToken(user._id.toString(), user.role);
const refreshToken = generateRefreshToken(user._id.toString(), user.tokenVersion);
// Store refresh token in HTTP-only cookie (for web)
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true, // Only in HTTPS
sameSite: "strict",
});
return res.json({ accessToken });
} catch (error) {
return res.status(500).json({ message: "Internal Server Error" });
}
};
refreshToken Controller
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import UserModel from "../models/User";
import { generateAccessToken, generateRefreshToken } from "../utils/jwt";
export const refreshToken = async (req: Request, res: Response) => {
const token = req.cookies.refreshToken || req.body.refreshToken;
if (!token) return res.status(401).json({ message: "Unauthorized" });
try {
const payload: any = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET!);
const user = await UserModel.findById(payload.userId);
if (!user || user.tokenVersion !== payload.tokenVersion)
return res.status(401).json({ message: "Unauthorized" });
const newAccessToken = generateAccessToken(user._id.toString(), user.role);
const newRefreshToken = generateRefreshToken(user._id.toString(), user.tokenVersion);
res.cookie("refreshToken", newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
});
return res.json({ accessToken: newAccessToken });
} catch (err) {
return res.status(401).json({ message: "Invalid token" });
}
};
logout Controller
export const logout = (req: Request, res: Response) => {
res.clearCookie("refreshToken");
return res.json({ message: "Logged out successfully" });
};
Middleware for Protecting Routes
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ message: "Unauthorized" });
try {
const payload: any = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ message: "Invalid token" });
}
};
Additional Security Considerations
Token Revocation:
Invalidate refresh tokens on password change or logout.
Store
tokenVersion
in the database, and increment it when needed.
Rate Limiting & Throttling:
- Use express-rate-limit to prevent brute-force attacks.
CSRF Protection:
- For web apps, use CSRF tokens or same-site cookies.
Secure Environment Variables:
- Store secrets in .env and never commit them to Git.
Final Thoughts
This approach ensures security, efficiency, and scalability.
Using access tokens for short sessions and refresh tokens for re-authentication reduces attack risks.
Following best practices for storage ensures safety across both web and mobile clients.
Conclusion
Access and refresh tokens play a crucial role in secure authentication. By implementing the right storage strategy and security best practices, you can enhance user experience and protect applications from common security threats like XSS and token theft.
By following this approach, your application can efficiently manage authentication while keeping user data secure.