The Developer's Quarterly · Web SecurityVol. II · February 2026

HttpOnly Cookies:How They Fit into React and Next.js Auth Flows

HttpOnly cookies keep session secrets out of client-side JavaScript, which makes them a strong default for React and Next.js apps that rely on server-validated authentication.

If you build authentication for a React app, the first design choice is rarely about login screens. It is usually about where the session token should live. Teams often compare localStorage, in-memory state, and cookies, but that discussion gets much clearer once the threat model is explicit. If the main concern is reducing the blast radius of cross-site scripting, HttpOnly cookies are usually the most practical default.

An HttpOnly cookie cannot be read from JavaScript. That one property changes the shape of the client-side code. A React component cannot pull the token out of document.cookie, cannot forward it into an Authorization header, and cannot accidentally leak it through logs or third-party scripts. The browser stores the cookie and attaches it to matching HTTP requests automatically.

In a Next.js application, that model fits well because the framework already has strong server-side boundaries: Route Handlers, Server Components, Server Actions, and middleware. Instead of teaching every client component how to manage tokens, you keep the sensitive credential on the server boundary and let the browser handle transport.

What HttpOnly actually protects

The HttpOnly attribute tells the browser that client-side JavaScript must not read the cookie. If an attacker injects code into the page through an XSS flaw, that script still runs, but it cannot directly extract the session value from the browser cookie jar.

1 Blocks token reads in JS

JavaScript cannot grab the cookie with document.cookie when the cookie is marked HttpOnly.

2 Does not stop all XSS damage

Injected scripts can still trigger actions or steal other data, so HttpOnly reduces risk but does not replace XSS defenses.

3 Changes app architecture

The client no longer owns the token lifecycle. The server becomes the place where session validation actually happens.

Why the model fits React

React itself does not need direct access to the session token. Most components only need to know whether the user is authenticated, which user profile to render, and whether protected actions succeed or fail. Those are state questions, not token-storage questions.

Browser stores cookie The credential stays in the browser transport layer instead of the component tree.
Server verifies session Your app validates the cookie on the server and returns user data or access decisions.
React renders outcomes The UI consumes session-derived state, not the raw secret.
Fewer client leaks There is no need for token persistence helpers, client loggers, or ad hoc auth headers everywhere.

The standard Next.js login flow

In a Next.js App Router project, the cleanest pattern is straightforward: a login request reaches a Route Handler, the server validates the credentials, the server sets an HttpOnly cookie, and future requests automatically include it. The client does not need to parse or persist the token at all.

Login route handler
import { NextResponse } from 'next/server'; export async function POST(request: Request) { const body = await request.json(); const { email, password } = body; const session = await authenticateUser(email, password); if (!session) { return NextResponse.json( { error: 'Invalid credentials' }, { status: 401 } ); } const response = NextResponse.json({ ok: true }); response.cookies.set('session', session.token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 7, }); return response; }

The cookie attributes are part of the security model, not decoration. httpOnly: true blocks JavaScript access, secure: true in production limits transport to HTTPS, and sameSite: 'lax' helps reduce common cross-site request scenarios.

Reading the cookie on the server

Once the cookie exists, Next.js server-side APIs can read it without exposing it to the client. In the App Router, cookies() is the standard entry point for that. A Server Component can enforce access control before protected UI is rendered.

Server component gate
import { cookies } from 'next/headers'; import { redirect } from 'next/navigation'; export default async function DashboardPage() { const cookieStore = await cookies(); const session = cookieStore.get('session'); if (!session) { redirect('/login'); } const user = await getUserFromSession(session.value); if (!user) { redirect('/login'); } return <div>Welcome back, {user.name}</div>; }

This is where the framework helps: the token stays on the server boundary, while the rendered component receives only validated application data.

What client components should do instead

A client component should not try to read the cookie. It should call your own API surface and let the browser include the cookie automatically. For same-origin requests, browser fetch does that for you.

Client login form
'use client'; import { useState } from 'react'; export function LoginForm() { const [error, setError] = useState(''); async function handleSubmit(formData: FormData) { setError(''); const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: formData.get('email'), password: formData.get('password'), }), }); if (!response.ok) { setError('Login failed'); return; } window.location.href = '/admin'; } return ( <form action={handleSubmit}> <input name="email" type="email" /> <input name="password" type="password" /> <button type="submit">Sign in</button> {error ? <p>{error}</p> : null} </form> ); }

Notice what disappears from the client code: no localStorage.setItem, no manual cookie parsing, and no global helper that injects bearer tokens into every request. The browser handles cookie transport, and the server handles session validation.

Where the model still needs help

The first mistake is assuming HttpOnly solves all authentication problems. It does not. Because the browser attaches cookies automatically, the remaining concern is often CSRF. That means state-changing routes still need deliberate protection.

1 Use SameSite deliberately

Lax is a strong default, while Strict may fit more controlled applications.

2 Add CSRF defenses

Tokens, origin checks, or referer validation are still relevant for sensitive write operations.

3 Keep cookies small

Store an opaque session id or compact signed token, not a whole profile payload.

The BFF pattern is usually cleaner

In many Next.js apps, the best structure is to treat the framework as a small backend-for-frontend. The browser calls your Next.js routes, those routes read the HttpOnly cookie, and the server layer talks to downstream services. That keeps authentication details out of the React tree and gives you one place to enforce authorization and logging.

Storage option JavaScript can read it Sent automatically Main tradeoff
localStorage Yes No Easy XSS token theft
Regular cookie Usually yes Yes XSS can still read it
HttpOnly cookie No Yes Needs CSRF-aware design

Logout is mostly cookie invalidation

Ending a session is straightforward: clear the cookie and, if sessions are tracked server-side, revoke the matching record there as well.

Logout route
import { NextResponse } from 'next/server'; export async function POST() { const response = NextResponse.json({ ok: true }); response.cookies.set('session', '', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 0, }); return response; }

If the application also stores sessions in a database, deleting the browser cookie is only half of the job. The server-side session record should be invalidated too.

Bottom line

In React and Next.js, HttpOnly cookies work best when you lean into the model instead of fighting it. Let the browser transport the cookie. Let Next.js server boundaries read and verify it. Let React render authenticated state based on server responses rather than direct token access.

That does not make the app magically secure, but it removes one of the most common and avoidable frontend authentication mistakes: handing the session secret to client-side JavaScript when the client never needed to hold it in the first place.