Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Cookie-Based Sessions for SSR Authentication

Hello! Welcome to your next lesson.

In our previous session, we established a powerful, automated data-fetching layer by generating type-safe React Query hooks from an OpenAPI specification. This allows us to fetch data efficiently during server-side rendering. However, most real-world applications have data that isn't public and requires user authentication.

Today, we will address that by implementing a secure and persistent authentication system. This lesson covers the following learning outcome:

Implement cookie-based session management for persistent authentication in an SSR context.

We will explore why traditional SPA authentication patterns (like storing tokens in localStorage) are not ideal for SSR and how to leverage secure, server-managed HttpOnly cookies to create a robust session management system within Tanstack Start.

1. The Foundation of SSR Authentication: Secure Cookies

In a Client-Side Rendered (CSR) application, it's common to receive a JWT from an API and store it in localStorage. The client then manually attaches this token to the Authorization header of subsequent requests. This approach has a significant vulnerability: if a Cross-Site Scripting (XSS) attack occurs, a malicious script can read the token from localStorage and impersonate the user.

In an SSR architecture, we have a much more secure alternative: HttpOnly cookies.

  • HttpOnly: This flag tells the browser that the cookie cannot be accessed via client-side JavaScript (document.cookie). It can only be sent by the browser in HTTP headers to the server. This immediately mitigates the risk of token theft via XSS.
  • Secure: This flag ensures the cookie is only sent over HTTPS, preventing it from being intercepted over an insecure connection.
  • SameSite: This attribute helps prevent Cross-Site Request Forgery (CSRF) attacks by controlling whether the cookie is sent with cross-origin requests.

Tanstack Start provides a built-in, high-level abstraction for managing secure, encrypted cookie sessions, which is the perfect foundation for our authentication system.

Let's look at the official documentation to see how to configure it.

Authentication | TanStack Start React Docs

The TanStack Start documentation provides a clear, concise example of how to set up and configure its session management utility. This will be the core of our implementation.

Please read section '2. Session Management' and the code block under '2. Session Security'. Pay close attention to the useSession function and the configuration options within the cookie object, especially httpOnly, secure, and sameSite.

As you can see, we can create a reusable useAppSession hook that configures our session cookie. The password option is used to encrypt and sign the cookie's content, ensuring that the data within it cannot be read or tampered with on the client.

2. Implementing Login and Logout with Server Functions

With our session utility configured, we can now implement the core authentication actions: login and logout. In Tanstack Start, this logic belongs on the server and is exposed to the client via createServerFn, which we've used before.

The flow is straightforward:

  1. Login: A client-side form submits credentials to a loginFn server function. The server validates the credentials, and if successful, uses session.update() to store user identification data (e.g., userId) in the session. This action creates the encrypted HttpOnly cookie and sends it back to the browser.
  2. Logout: The client calls a logoutFn server function. The server uses session.clear() to destroy the session data and instruct the browser to delete the cookie.

The Tanstack documentation provides direct examples of this pattern.

Authentication | TanStack Start React Docs

Let's refer back to the Tanstack Start docs to see how to implement the login and logout server functions that interact with the session we just configured.

Please review the code block under '1. Server Functions for Authentication'. Observe how loginFn calls session.update() upon successful authentication and logoutFn calls session.clear().

This simple mechanism is powerful. On every subsequent request to the server (either a page navigation or a call to a server function), the browser will automatically include the session cookie, allowing our server-side code (like route loaders) to access the session data and identify the user.

3. A Deeper Dive: The Access and Refresh Token Strategy

While the useSession utility is a great abstraction, it's important to understand the robust security pattern it often manages: access and refresh tokens. This strategy is fundamental to modern persistent authentication.

  • Access Token: A short-lived token (e.g., 15 minutes) that grants access to protected resources. Its short lifespan limits the damage if it's ever compromised.
  • Refresh Token: A long-lived token (e.g., 7 days) whose only purpose is to obtain a new access token. It is stored securely and used infrequently.

The video below gives an excellent overview of this strategy. While it focuses on a client-side React implementation, the core principles are universal and highly relevant.

React Login Authentication with JWT Access, Refresh Tokens, Cookies and Axios

This video from Dave Gray, 'React Login Authentication with JWT Access, Refresh Tokens...', clearly explains the roles of access and refresh tokens and the security rationale behind their use.

Watch the section 'JWT Strategy Overview and Token Storage' and the final summary. Focus on the distinction between the two tokens and the recommended storage for each: the access token in memory and the refresh token in an HttpOnly cookie.

In our SSR architecture with Tanstack Start, we can implement this perfectly:

  • The Refresh Token is the ideal candidate for storage inside our encrypted, HttpOnly session cookie managed by useSession. It remains secure on the server and is never exposed to client-side scripts.
  • The Access Token can also be stored in the server session. When a page is rendered, the server can use it to fetch data. For client-side API calls, we'll later see how to pass this token from the server to the client's memory.

Let's see an analogous implementation in a Next.js app. This video demonstrates creating an encrypted JWT that acts as the session cookie, which is exactly what Tanstack's useSession does under the hood.

Next.js App Router Authentication (Sessions, Cookies, JWTs)

This video by Lee Robinson shows how to create and set an encrypted session JWT as an HttpOnly cookie in Next.js. This provides a great visual and conceptual parallel to what we are doing in Tanstack Start.

Watch the segments on 'login and session creation' and 'encrypting JWTs'. Notice how a session object containing user data is encrypted and set as a cookie with the httpOnly: true flag.

4. Managing the Session Lifecycle

Now, let's connect these concepts to build a full, persistent session lifecycle.

Session Data Structure

Instead of just storing a userId, our session can hold the entire authentication state, including the tokens.

// src/lib/session.ts
import { useSession } from "@tanstack/react-start/server";

type AuthenticatedAppSession = {
  status: "authenticated";
  user: { id: string; email: string; };
  accessToken: string;
  accessTokenExpiresAt: number;
  refreshToken: string;
};

// ... other session types

export function getAppSession() {
  return useSession<AuthenticatedAppSession | ...>({
    name: 'app-session',
    password: process.env.SESSION_SECRET!,
    // ... cookie config
  });
}

This structure, inspired by the Medium article on Tanstack Start authentication (resource fa3ab), allows our server-side logic to have all the necessary information in one place.

Automatic Token Refresh

A key part of "persistent" authentication is seamlessly refreshing the access token when it expires, without forcing the user to log in again. This can be done on the server before a page loads.

The following article, though for an OIDC flow, demonstrates a powerful pattern in its getAuthFn function: before returning the user's auth status, it checks if the access token is about to expire. If it is, it triggers a refresh flow on the server, updates the session with the new tokens, and then proceeds.

TanStack Start Authentication with OIDC/OAuth 2.0 ...

This article on Tanstack Start authentication shows an advanced server-side pattern for automatically refreshing an access token.

Please read the section 'Refresh token handler'. Focus on the getAuthFn example. Notice the check if (session.data.accessTokenExpiresAt <= (Date.now() + 5_000)) which proactively refreshes the token on the server.

This server-side refresh is highly secure and efficient, as it happens transparently before the client receives the page.

Proper Logout

A secure logout involves more than just clearing the local session. If your authentication provider supports it, you should also revoke the refresh token to invalidate it completely.

TanStack Start Authentication with OIDC/OAuth 2.0 ...

Let's look at the same article to see a complete logout implementation.

Review the 'Logout handler' section. The logoutFn first attempts to revoke the refresh token with the authentication server (oidc.revokeToken) and then, in a finally block, ensures the local session is cleared with session.clear().

This two-step process ensures that even if the refresh token were somehow compromised, it could no longer be used.

Conclusion

Today we have built a secure foundation for authentication in our SSR application. By moving away from client-side token storage and embracing server-managed sessions with HttpOnly cookies, we have significantly improved the security posture of our app.

Key Takeaways:

  • HttpOnly Cookies are the Standard: They are the cornerstone of secure session management in SSR, preventing token theft via XSS.
  • Tanstack Start's useSession: This utility provides a simple yet powerful API for managing encrypted, signed session cookies.
  • Server Functions for Auth Logic: createServerFn is the ideal tool for implementing server-side login and logout actions that manipulate the user's session.
  • Access/Refresh Token Pattern: This strategy provides both security (short-lived access tokens) and persistence (long-lived, securely stored refresh tokens).
  • Server-Side Session Lifecycle: In an SSR app, critical logic like token refreshes can and should happen on the server, providing a seamless and secure experience for the user.

Next Up:

We've successfully established a persistent server-side session. The next step is to securely manage the lifecycle of the tokens within that session and bridge the authentication state to the client-side of our application. In the next lesson, we will focus on: Securely manage authentication tokens (access and refresh) using server-side logic and HTTP-only cookies. We will dive deeper into token generation, validation, and creating a React context to make the user's auth status available throughout the client-side components.

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

Sign up