Hello! Welcome back to our course on mastering TanStack Start.
In our last lesson, we laid the groundwork for a robust session management system by modifying the login process to issue two tokens: a short-lived access token and a long-lived refresh token, both stored in secure, httpOnly cookies.
Today, we will build the other half of this mechanism. This lesson focuses on creating the dedicated server function that handles refresh token requests. This is not just about exchanging one token for another; we will implement two critical security patterns: refresh token rotation and reuse detection. These patterns significantly enhance the security of your application by minimizing the risk associated with a compromised refresh token.
By the end of this lesson, you will be able to create a dedicated server function that securely validates a refresh token, issues a new access/refresh token pair, and detects potential token theft.
The Refresh Token Endpoint: An Overview
The core of today's lesson is a new server function, which will serve as our refresh endpoint (e.g., /api/auth/refresh). Its job is to:
- Receive a request containing the
refreshTokenfrom its cookie. - Validate the token against the database to ensure it's legitimate and hasn't been used.
- If valid, "rotate" it by issuing a new access token and a new refresh token.
- Invalidate the old refresh token.
- If an already-used token is presented, detect this as a security threat and take immediate action.
This process is a significant security upgrade over simply re-issuing an access token. To get a clear conceptual understanding of refresh token rotation and the powerful idea of reuse detection, please start by watching the introduction of this excellent video.
Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication
This video from Dave Gray provides a practical, step-by-step guide to implementing refresh token rotation and reuse detection in a Node.js environment, which is highly analogous to our work in TanStack Start.
Watch the 'Introduction to Refresh Token Rotation and Reuse Detection' from 00:38 to 02:20. Focus on how rotation reduces the risk of a compromised token and what 'reuse detection' means.
1. The Core Logic of the Refresh Function
Let's design our server function. It will receive the refresh token from the Cookie header of the incoming request. Here is the sequence of checks it must perform:
- Token Existence: Does the
refreshTokencookie exist? If not, the request is unauthorized (401). - Database Lookup: Find the user associated with this refresh token in your database. In the previous lesson, we discussed persisting refresh tokens. A common approach is to store an array of active refresh tokens for each user, allowing for multiple simultaneous sessions (e.g., on a laptop and a phone).
- Token Validity: Verify the JWT's signature and expiration using your
JWT_REFRESH_SECRET. If it fails verification (e.g., it's expired or tampered with), the token is invalid (403 Forbidden). The invalid token should be removed from the user's record in the database. - Rotation and Issuance: If all checks pass, generate a new access token and a new refresh token. Update the database by removing the old refresh token and adding the new one.
- Response: Send the new access token in the JSON response body and the new refresh token in a secure,
httpOnlycookie.
This article provides a concise explanation and a clear diagram of this flow.
Refresh Token Rotation: Best Practices for Developers
The article 'Refresh Token Rotation: Best Practices for Developers' from Serverion offers a great summary of the token rotation process and the checks involved.
Read the section 'How Refresh Tokens Work', focusing on the 'Token Rotation Process' subsection and the table of checks (Token Reuse Detection, Grace Period, Token Family Validation). This will solidify your understanding of the 'one-time use' principle.
2. Implementing Refresh Token Rotation
Refresh token rotation means that every time a refresh token is used, it is invalidated and replaced with a new one. This ensures each refresh token is a single-use credential.
Let's see how this is implemented in code. The following video segment walks through the exact logic for finding the old token, generating a new pair, and updating the database.
Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication
Continuing with Dave Gray's tutorial, this part demonstrates the implementation of the rotation logic within the refresh controller.
Watch the segment from 11:15 to 16:31. Pay close attention to how the code: Filters the old refresh token out of the user's array of tokens. Generates a newAccessToken and a newRefreshToken. Saves the updated array (containing the new token but not the old one) back to the database. Sets the newRefreshToken in a new httpOnly cookie.
The key operation here is updating the user's token list in the database. If you're storing tokens in an array on the user document, the logic is:
// 1. Filter out the used refresh token
const newRefreshTokenArray = foundUser.refreshTokens.filter(
(rt) => rt !== incomingRefreshToken
);
// 2. Generate new tokens
const newAccessToken = jwt.sign(...);
const newRefreshToken = jwt.sign(...);
// 3. Update the user's token list in the database
foundUser.refreshTokens = [...newRefreshTokenArray, newRefreshToken];
await foundUser.save();
// 4. Set the new cookie and send the new access token
// ...
3. The Ultimate Security Upgrade: Reuse Detection
What happens if an attacker steals a refresh token and uses it before the legitimate user does?
With simple rotation, the attacker gets a new set of tokens, and the legitimate user is logged out when their now-invalid token is rejected. This is good, but we can do better. We can use this event to detect the attack and lock down the user's account.
This is reuse detection: If the server receives a refresh token that it recognizes but which is no longer in the user's active token list, it's a guaranteed sign of token reuse. The original has already been used and rotated.
The Correct Response: When reuse is detected, the server should assume the user's account is compromised and immediately invalidate all active refresh tokens for that user. This forces a logout on all devices and contains the breach.
This next video segment is crucial as it demonstrates exactly how to implement this logic.
Refresh Token Rotation and Reuse Detection in Node.js JWT Authentication
This is the most important security aspect of our lesson. This segment from Dave Gray's video shows how to handle a reuse attempt by invalidating all of a user's sessions.
Watch the segment from 05:50 to 11:15. This part handles the else block of if (foundUser). Focus on: The logic: if the token was received but no user was found with that active token, it implies reuse. The code decodes the (now invalid) token just to get the username or userId. It then finds the 'hacked user' by that identifier. It sets the user's refreshToken array to be empty, effectively logging them out everywhere. Finally, it saves the user and returns a 403 Forbidden error.
This article also provides a great code example of this check, using a last_used_at field instead of removing the token from an array. Both are valid strategies.
The Ultimate Guide to JWT server-side auth (with refresh ...
This guide from dev.to shows an alternative implementation of reuse detection, which is also very effective.
Review the code snippet for the handleRefreshTokens function. Notice the check if (token.last_used_at). If this field is already populated, it means the token has been used before, triggering the logic to block the user's account. This is conceptually identical to the video's approach.
Summary: The Complete Server Function
Let's assemble the full logic for our TanStack Start server function.
import { server$ } from "@tanstack/start/server";
import { getCookie, setCookie } from "vinxi/http";
import jwt from "jsonwebtoken";
// Assume User model and DB connection are set up
export const refreshAccessToken = server$(async function () {
const refreshToken = getCookie("refreshToken");
if (!refreshToken) {
// No refresh token provided
throw new Error("Unauthorized"); // This will result in a 401
}
// Find the user who owns this refresh token
const foundUser = await User.findOne({ refreshTokens: refreshToken }).exec();
// DETECT REUSE: Token exists but is not associated with any user's active list
if (!foundUser) {
try {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// It's a valid JWT, so this is a definite reuse attempt.
// Invalidate all refresh tokens for this user
const hackedUser = await User.findOne({ _id: decoded.userId }).exec();
if (hackedUser) {
hackedUser.refreshTokens = [];
await hackedUser.save();
}
} catch (err) {
// Token was invalid anyway, do nothing.
}
throw new Error("Forbidden"); // 403
}
// At this point, we have a valid user with a valid, active refresh token.
const newRefreshTokenArray = foundUser.refreshTokens.filter(rt => rt !== refreshToken);
try {
// Evaluate the JWT
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
// Ensure the user from the token matches the user we found
if (foundUser._id.toString() !== decoded.userId) {
throw new Error("Forbidden"); // 403
}
// ROTATE: Issue new tokens
const newAccessToken = jwt.sign(
{ userId: foundUser._id, role: foundUser.role },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: "15m" }
);
const newRefreshToken = jwt.sign(
{ userId: foundUser._id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: "7d" }
);
// Save the new refresh token to the DB
foundUser.refreshTokens = [...newRefreshTokenArray, newRefreshToken];
await foundUser.save();
// Set the new refresh token cookie
setCookie("refreshToken", newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
// Return the new access token
return { accessToken: newAccessToken };
} catch (err) {
// The refresh token was expired or invalid
// Remove it from the database
foundUser.refreshTokens = newRefreshTokenArray;
await foundUser.save();
throw new Error("Forbidden"); // 403
}
});
Conclusion
You have now designed and implemented a secure, production-ready token refresh endpoint. This server-side logic is the backbone of a seamless and safe user experience.
Key Takeaways:
- A dedicated server function is needed to handle the logic of exchanging a refresh token for a new access token.
- Refresh Token Rotation is a vital security pattern where a new refresh token is issued and the old one is invalidated upon every successful refresh.
- Reuse Detection provides a powerful defense against token theft by identifying when a stolen, used token is presented again, allowing you to revoke all sessions for the compromised user.
- The implementation involves careful database management to track active tokens and coordinated JWT generation and verification.
What's Next?
Our server is now fully equipped to handle token refreshes securely. However, the client-side application is still unaware of this mechanism. In the next lesson, we will bridge this gap by implementing a client-side interceptor. This interceptor will automatically catch API errors due to expired access tokens, call our new refresh endpoint, and seamlessly retry the original request with the new token.
Can't find a good explanation? Sign up and we'll make it for you
Sign up