Refresh tokens allows you to issue short-lived access tokens and longer-lived refresh tokens. When the access token expires, the client can send the refresh token to get a new access token. This provides an additional layer of control for logout or token expiration.
- Access Token: Short-lived (e.g., 15 minutes), used to access protected resources.
- Refresh Token: Long-lived (e.g., 30 days), used to request new access tokens after expiration.
Refresh Token Flow:
-
User Logs In:
- The server sends both an access token (JWT) and a refresh token.
-
User Logs Out:
- The client removes both the access token and refresh token.
- The server can invalidate the refresh token (e.g., remove it from the database).
-
Access Token Expiry:
- When the access token expires, the client can use the refresh token to obtain a new access token from the server.
This approach allows for more robust logout functionality, as refresh tokens can be invalidated server-side, ensuring that the user can't continue using old tokens.
2. Why Use Refresh Tokens?
Refresh tokens help you separate concerns of authentication (verifying a user's identity) and session management (keeping a user logged in over time) while maintaining security.
Key Benefits:
-
Short-Lived Access Tokens (Access Control):
- If the access token is short-lived (e.g., 15 minutes), it limits the exposure time of an access token.
- If an attacker steals the access token, the damage is limited by its short lifespan.
-
Long-Lived Refresh Tokens (Reauthentication):
- Refresh tokens allow the user to stay logged in without needing to authenticate again after the access token expires.
- Refresh tokens are kept securely (on the client side, usually in an httpOnly cookie), and only sent to the server when a new access token is needed.
3. How the Client Handles Refresh Tokens
Here's the flow for handling refresh tokens on the client side, assuming you're using JWTs.
Login Flow:
- User logs in with their credentials (email + password).
-
The server responds with:
- Access Token (short-lived, e.g., 15 minutes)
- Refresh Token (long-lived, e.g., 30 days)
-
The client stores:
- The access token (e.g., in memory, localStorage, or a cookie for API calls).
- The refresh token (preferably in an httpOnly cookie, which is less vulnerable to XSS attacks).
Requesting New Access Token with Refresh Token:
- When the client makes a request to a protected route, it includes the access token in the
Authorization
header. - If the access token expires, the client will send the refresh token to the server to get a new access token:
- The server verifies the refresh token.
- If valid, the server issues a new access token and sends it back to the client.
- The server may also refresh the refresh token itself, issuing a new one with each access token renewal (to prevent long-term use of an old refresh token).
Logout Flow:
- To log out, the client can:
- Remove both the access token and refresh token from the client (i.e., from localStorage or cookies).
- Optionally, the client can notify the server to invalidate the refresh token (by blacklisting or removing it from the database).
4. How to Implement Refresh Tokens on the Client-Side
Hereβs how you would manage the refresh token on the client side, using localStorage, sessionStorage, or httpOnly cookies.
1. Storing the Tokens
-
Access Token:
- You may store it in localStorage or sessionStorage to make it easily available for API requests. However, this could expose the token to XSS attacks if the site is vulnerable.
- Alternatively, you can store it in memory (in a JS variable) while the app is running (but this means the user will need to log in again if the page is refreshed).
-
Refresh Token:
- httpOnly cookies are the most secure option for storing refresh tokens, as it prevents access to the refresh token via JavaScript (protecting from XSS attacks).
- Example:
Set-Cookie: refreshToken=<value>; HttpOnly; Secure; SameSite=Strict
.
2. Making API Requests with the Access Token
For each request to a protected route, you send the access token in the Authorization
header:
fetch('https://api.example.com/protected', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`, // Attach the access token
},
})
.then(response => response.json())
.then(data => {
// Handle the API response
})
.catch(error => {
// Handle errors
});
3. Handling Access Token Expiry and Refreshing
If the access token expires, you can request a new one by using the refresh token.
- First, check if the access token has expired.
- If expired, send a request to your backend to refresh the token.
Example refresh token request:
// Function to get a new access token using refresh token
function refreshAccessToken() {
return fetch('https://api.example.com/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(data => {
const { accessToken, refreshToken: newRefreshToken } = data;
// Store the new tokens (in memory, localStorage, etc.)
storeTokens(accessToken, newRefreshToken);
})
.catch(error => {
// Handle error, e.g., if refresh token is invalid or expired
});
}
4. Removing Tokens on Logout
To log out, you simply remove both the access token and refresh token from wherever they are stored:
// Remove from localStorage
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// Remove from cookies
document.cookie = 'refreshToken=; Max-Age=0; path=/;';
5. Server-Side: Handling Refresh Tokens
When the client sends a refresh token to get a new access token, the server will:
- Verify the refresh token (check its signature, expiration, etc.).
- If valid:
- Issue a new access token.
- Optionally issue a new refresh token.
- If the refresh token is expired or invalid, deny the request and require the user to log in again.
Example Express Route for Refreshing Tokens:
// src/routes/refreshTokenRoute.ts
import express from 'express';
import { verifyRefreshToken, generateAccessToken } from '../utils/jwtUtils';
import { getRefreshTokenFromCookies } from '../utils/cookieUtils';
const router = express.Router();
router.post('/refresh', async (req, res) => {
const refreshToken = getRefreshTokenFromCookies(req);
if (!refreshToken) {
return res.status(401).json({ message: 'Refresh token missing' });
}
try {
const user = await verifyRefreshToken(refreshToken); // Verify refresh token
const newAccessToken = generateAccessToken(user.id); // Generate a new access token
const newRefreshToken = generateRefreshToken(user.id); // Optionally, generate a new refresh token
res.json({
accessToken: newAccessToken,
refreshToken: newRefreshToken, // Optionally send the new refresh token
});
} catch (error) {
res.status(403).json({ message: 'Invalid refresh token' });
}
});
export default router;
Conclusion
Refresh tokens provide a more secure and flexible approach for managing long-lived user sessions in a stateless authentication system. They allow you to:
- Keep access tokens short-lived for security (limiting exposure if the token is stolen).
- Use refresh tokens to request new access tokens without requiring the user to log in repeatedly.
- Ensure logout by simply removing both tokens from the client-side storage.
This separation between access tokens and refresh tokens gives you more control over security and user experience.
What are your thoughtsπ?
Top comments (0)