Recommended resources
The freely available resources this lesson will direct you to.
- Why HttpOnly Cookies Are SAFER Than Local Storage! | Using HttpOnly Cookie With JWT .Net 6 APIwww.youtube.com
- Using HttpOnly cookies in React & Node | Storing JWT Tokens or SessionID Securelywww.youtube.com
- Secure Role-Based Access Control in React with HTTP ...medium.com
- Backend Authentication with Next.js, Prisma, JWT, Cookies ...medium.com
Hello! Welcome to the first lesson in our module on JWT Authentication in an SSR context.
Introduction
In the previous modules, we explored the fundamentals of TanStack Start and Router, focusing on routing, data loading, and the distinction between server and client code. Now, we'll apply those concepts to one of the most critical features of any modern web application: secure user authentication.
This lesson addresses the learning outcome: Store a JWT securely in an HTTP-only cookie upon successful login.
We will cover:
- Why storing authentication tokens in
localStorageis risky. - What HTTP-only cookies are and how they provide enhanced security.
- How to implement a server function in TanStack Start that, upon successful login, generates a JWT and sets it in a secure, HTTP-only cookie.
This is a foundational step for building a robust authentication system. Let's get started.
1. The Problem: Storing Tokens on the Client
In many Single-Page Applications (SPAs), it's common to store JSON Web Tokens (JWTs) in the browser's localStorage after a user logs in. While convenient, this practice exposes a significant security vulnerability.
To understand why, let's first compare the different browser storage mechanisms.
Using HttpOnly cookies in React & Node | Storing JWT Tokens or SessionID Securely
This video provides an excellent overview of the differences between cookies, local storage, and session storage, and introduces why HttpOnly cookies are a more secure choice for sensitive data like authentication tokens.
Watch the first 5 minutes of the video (00:00 - 04:52). Focus on the comparison between local storage and cookies, particularly the discussion around security and accessibility via JavaScript.
As the video explains, data in localStorage and sessionStorage is accessible via JavaScript. This means if your application has a Cross-Site Scripting (XSS) vulnerability, a malicious script injected into your page can read the JWT from localStorage and send it to an attacker. With that token, the attacker can impersonate the user.
This is where HTTP-only cookies come in.
2. The Solution: Secure, HTTP-only Cookies
An HTTP-only cookie is a special type of cookie that cannot be accessed through client-side scripts. When a browser receives a cookie with the HttpOnly flag, it will store it, but it will prevent any JavaScript code running on the page (document.cookie) from accessing it.
However, the browser will automatically include the cookie in the headers of every subsequent HTTP request made to the server (specifically, to the domain that set the cookie).
This simple mechanism is incredibly effective:
- It mitigates XSS token theft: A malicious script can't steal the token because the browser denies access.
- It's managed by the server: HTTP-only cookies can only be created and modified by the server, giving you full control over the session.
- It's handled automatically: You don't need to write client-side code to attach the token to every API request; the browser does it for you.
Essential Cookie Attributes for Security
Besides HttpOnly, several other attributes are critical for securing your authentication cookie.
Using HttpOnly cookies in React & Node | Storing JWT Tokens or SessionID Securely
Let's continue with the same video to understand the key security attributes for cookies.
Watch the segment from 04:52 to 08:40. This part covers the SameSite and Secure flags, and then dives deeper into the HttpOnly flag, explaining exactly how it protects against XSS.
To summarize, here are the key attributes you'll use when setting your authentication cookie:
HttpOnly: Prevents access from client-side JavaScript, protecting against XSS.Secure: Ensures the cookie is only sent over HTTPS, protecting against man-in-the-middle attacks. You should always use this in production.SameSite=Strict(orLax): A crucial defense against Cross-Site Request Forgery (CSRF) attacks.Strictis the most secure option, preventing the cookie from being sent on any cross-site navigation.Max-AgeorExpires: Determines the cookie's lifetime, controlling the user's session duration.Path=/: Makes the cookie available across your entire site.
3. Implementation in TanStack Start
Now, let's see how to implement this in a TanStack Start application. The process happens entirely on the server within a server function.
Let's imagine we have a login form that posts a user's credentials. We'll create a server function, for example in app/routes/login.server.ts, to handle this submission.
The logic is as follows:
- Receive the user's credentials (e.g., email and password) from the form submission.
- Validate the credentials against your database.
- If valid, generate a JWT containing user information (like
userId). - Set this JWT inside a secure, HTTP-only cookie in the HTTP response.
- Return a success response or redirect the user.
The article below demonstrates this flow within a Next.js API route. The code and concepts are directly transferable to a TanStack Start server function, as both run in a Node.js environment on the server.
Backend Authentication with Next.js - Login Route
This article provides a clear, practical code example for creating a login endpoint that sets a JWT in a secure, HttpOnly cookie. We will focus on the login implementation.
First, read section 5, 'Creating the Auth API Routes (TypeScript) - Login'. Pay close attention to the code block and the comments explaining how the cookie is configured with httpOnly, secure, sameSite, etc. Then, read section 6, 'Why Cookies?', which provides an excellent summary of the security benefits.
Example: TanStack Start Login Server Function
Drawing from the principles in the resources, here is what a login server function might look like in a TanStack Start project. This function would typically be created using server$().
For this example, assume you have bcrypt for password hashing and jsonwebtoken for JWTs installed.
// app/routes/login.server.ts
import { server$ } from "@tanstack/react-start/server";
import { redirect } from "@tanstack/react-router";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { db } from "~/db"; // Your hypothetical database client
import { setCookie } from "vinxi/http"; // Vinxi's helper for setting cookies
export const login = server$(async (formData: FormData) => {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// 1. Validate input
if (!email || !password) {
throw new Error("Email and password are required.");
}
// 2. Find user and validate credentials
const user = await db.user.findUnique({ where: { email } });
if (!user) {
throw new Error("Invalid credentials.");
}
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
throw new Error("Invalid credentials.");
}
// 3. Generate JWT
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET!, // Ensure you have a secret in your .env
{ expiresIn: "1h" }
);
// 4. Set the secure, HTTP-only cookie
setCookie("auth_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
path: "/",
maxAge: 60 * 60, // 1 hour
});
// 5. Redirect to a protected page on success
throw redirect({
to: "/dashboard",
// This ensures the cookie is set before the browser is redirected
headers: {
"Set-Cookie": `auth_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600`,
},
});
});
Note: The setCookie helper from Vinxi/H3 is a convenient way to manage cookies. The explicit Set-Cookie header in the redirect is shown to make the mechanism clear and ensure it works reliably with redirects.
When this server function executes, the user's browser will receive the Set-Cookie header and store the auth_token cookie. Because of the HttpOnly flag, your client-side React code will not be able to see or touch this token, but it will be sent automatically on all future requests to your server.
Conclusion
In this lesson, we established a secure foundation for our authentication system.
Key Takeaways:
- Storing JWTs in
localStorageis insecure due to vulnerability to XSS attacks. - HTTP-only cookies are the industry-standard solution, as they prevent client-side scripts from accessing the token.
- Setting a cookie is a server-side action. In TanStack Start, this is done within a server function (
server$). - For maximum security, a token cookie must be configured with the
HttpOnly,Secure, andSameSite=Strictattributes.
Next Up:
Now that we have successfully stored the token in the user's browser, the next logical step is to use it. In the next lesson, we will learn how to implement server-side token validation in middleware to protect our API endpoints and server functions, ensuring only authenticated users can access them.