Create your own
Lesson illustration

Automatic Token Refresh in Server Middleware

Hello! Welcome back to our course on mastering TanStack Start.

In our last lesson, we built a crucial piece of our security infrastructure: a server function that handles token refreshes using rotation and reuse detection. Now, we need to create the mechanism that decides when to use it.

This lesson focuses on implementing the server-side detection of an expired access token. We will create a middleware function that intercepts incoming requests to protected server loaders and API endpoints. This middleware will be responsible for validating the access token and, most importantly, signaling back to the client when it has expired.

By the end of this lesson, you will be able to implement server middleware that automatically detects expired access tokens and responds in a way that enables the client to trigger a refresh.

1. The Role of Middleware in Authentication

In a server-rendered application, middleware is code that runs on the server before a request reaches its final destination, such as a route loader or an API handler. This makes it the perfect place to centralize logic that needs to run on every request, like authentication.

Our goal is to create a middleware that:

  1. Intercepts requests to protected routes.
  2. Extracts the JWT access token from the request headers.
  3. Verifies the token's signature and expiration.
  4. If the token is valid, it allows the request to proceed.
  5. If the token is invalid or expired, it halts the request and sends back a specific error status (401 Unauthorized).

To understand the core concepts of middleware in a modern SSR framework, the patterns used in Next.js are highly analogous to what we'll do in TanStack Start (which uses the Vinxi bundler).

Implementing JWT Middleware in Next.js: A Complete Guide ...

This article from Leapcell provides an excellent overview of how middleware works in Next.js. The principles are directly applicable to our task in TanStack Start.

Please read Section III, 'Next.js Middleware: Core Mechanisms and Advantages', and Section 6.3, 'Analysis of the Core Logic of the Middleware'. Focus on understanding how middleware intercepts requests and the core checks involved: token existence, signature verification, and expiration.

2. The Core Verification Logic

At the heart of our middleware is the jsonwebtoken library's verify function. This function takes the token, the secret key, and an optional callback. It automatically checks for both a valid signature and whether the token has expired based on its exp claim.

When jwt.verify encounters an expired token, it throws a specific error named TokenExpiredError. This is the exact signal we need to catch.

This video segment provides a clear, simple example of creating an authentication middleware in a Node.js/Express environment. The logic for extracting and verifying the token is identical to what we'll implement.

JWT Authentication Tutorial - Node.js

This clip from Web Dev Simplified's JWT tutorial demonstrates a classic token authentication middleware. Pay attention to how the token is extracted and how jwt.verify is used with a callback to handle success and error cases.

Watch the segment from 10:10 to 13:57. Focus on the authenticateToken function. Note how it gets the token from the Authorization header and uses jwt.verify. The error handling in the callback is the key part: if there's an error (like an expired token), it returns a 403 Forbidden status. We will adapt this to return a 401 Unauthorized status specifically for expired tokens.

3. Implementing the Middleware in TanStack Start

Now, let's translate these concepts into a TanStack Start application. TanStack Start uses Vinxi, which allows us to define middleware that can be applied to our server-side routes. We'll create a middleware function that performs our authentication check.

The critical logic is within a try...catch block around jwt.verify:

  • try block: If jwt.verify succeeds, it means the token is valid. We can attach the decoded user payload to the request context for later use in our loaders or server functions and allow the request to proceed.
  • catch block: If jwt.verify fails, we inspect the error.
    • If error.name === 'TokenExpiredError', we know the client needs to refresh. We'll stop the request and return a 401 Unauthorized response.
    • For any other error (e.g., invalid signature, malformed token), or if no token was provided at all, we'll also return a 401 status.

Here is what a complete middleware implementation could look like in a TanStack Start project. You might place this in a file like src/middleware.ts.

// src/middleware.ts
import { eventHandler, H3Event, parseCookies, sendError } from "vinxi/http";
import jwt from "jsonwebtoken";

// Define a type for our JWT payload for type safety
interface JwtPayload {
  userId: string;
  // Add other properties you have in your payload, e.g., roles
  [key: string]: any;
}

// A helper function to verify the token
const verifyAccessToken = (token: string): JwtPayload | null => {
  try {
    const decoded = jwt.verify(token, process.env.VITE_JWT_ACCESS_SECRET!) as JwtPayload;
    return decoded;
  } catch (error) {
    // Re-throw the error to be handled by the caller
    throw error;
  }
};

export default eventHandler(async (event: H3Event) => {
  // We only want to run this middleware on specific paths, e.g., /api/protected/*
  const path = event.path;
  if (!path.startsWith("/api/protected")) {
    // Not a protected route, do nothing.
    return;
  }

  const authHeader = event.node.req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN

  if (!token) {
    // No token provided
    return sendError(event, new Error("Unauthorized: No token provided"), 401);
  }

  try {
    const decodedPayload = verifyAccessToken(token);
    
    // Token is valid. Attach payload to the event context for use in route handlers.
    // This allows you to access `event.context.user` in your server functions.
    event.context.user = decodedPayload;

    // Let the request proceed to the actual handler
    return;

  } catch (error: any) {
    // Check if the error is due to token expiration
    if (error.name === 'TokenExpiredError') {
      // This is the specific signal our client will look for to trigger a refresh.
      return sendError(event, new Error("Unauthorized: Token expired"), 401);
    }

    // For any other verification error (invalid signature, etc.)
    return sendError(event, new Error("Unauthorized: Invalid token"), 401);
  }
});

Note: To apply this middleware, you would configure it in your app.config.ts file within the server router definition, pointing to the paths you want to protect.

This middleware effectively acts as a gatekeeper for your server-side logic. It doesn't perform the refresh itself; its job is to detect the need for a refresh and signal it clearly to the client.

Conclusion

You have now implemented the server-side component of our automatic token refresh mechanism. The middleware acts as a centralized, secure checkpoint for all protected server resources.

Key Takeaways:

  • Middleware is the ideal place to implement centralized authentication logic in an SSR application.
  • The jsonwebtoken library's verify function automatically checks for token expiration.
  • Catching the TokenExpiredError is the key to detecting when an access token has expired.
  • The correct server-side response to an expired access token is to send a 401 Unauthorized status, signaling the client to initiate the refresh flow.
  • A valid token's payload can be attached to the request context for easy access in downstream handlers.

What's Next?

Our server can now reliably detect and report expired tokens. The final step is to make the client application react to this signal. In our next lesson, we will implement a client-side interceptor. This powerful pattern will allow us to automatically catch the 401 error from our API calls, seamlessly trigger our token refresh function, and retry the original failed request without the user ever noticing.

Can't find a good explanation? Sign up and we'll make it for you

Sign up