Logo

Building Secure Login Systems with HttpOnly Cookies

How HttpOnly cookies with CHIPS prevent XSS attacks and cross-site tracking while maintaining seamless authentication across subdomains.

Rob Helmer

Rob Helmer

3/17/2026 · 7 min read

Tags:


Illustration of an astronaut cat in a spaceship being protected by secure cookies from alien attack.

At Stellar Whiskers, I run multiple subdomains (accounts. for auth, games. for cloud save/load, and www. for the main site and game pages)—all while keeping the experience tracker-free and privacy-first. Here’s how I make that work securely.

The Overall Architecture

Before diving into cookie flags and security properties, let me explain the overall flow. This establishes the logic of what’s happening and why the design choices matter.

sequenceDiagram
    participant U as User
    participant C as Browser
    participant A as Auth Server<br/>(accounts.stellarwhiskers.com)
    participant G as Game Server<br/>(games.stellarwhiskers.com)

    U->>C: Enter credentials on auth page
    C->>A: POST /login with credentials
    A-->>C: Set-Cookie: auth=session_token
    Note over C,A: Cookie is HttpOnly, Secure,<br/>SameSite=Lax, Partitioned
    C->>G: Join game (browser sends cookie automatically)
    G->>A: Validate session token
    A-->>G: Valid user ID
    G-->>C: Game joined
    Note over C,A: Cookie never accessible to JavaScript

The key insight: the auth server sets a cookie, and then that cookie is automatically sent to the game server when the user navigates there. The game server never sees credentials—it only validates sessions with the auth server. This separation is critical for security.

The Problem We’re Solving

Many modern web apps use JWT tokens stored in localStorage, which is vulnerable to XSS attacks. Any script running on your page can read localStorage and steal the token.

Traditional JWT-based auth stores tokens in localStorage:

// VULNERABLE to XSS
localStorage.setItem('auth_token', token);

// Any script can read it
const token = localStorage.getItem('auth_token');

If an attacker injects malicious JavaScript (via XSS vulnerability, compromised dependency, or malicious browser extension), they can steal the token and impersonate the user.

Additionally, Stellar Whiskers uses multiple subdomains that need to share authentication state. These are considered cross-site by browsers, which creates both UX and security challenges.

The Solution: HttpOnly Cookies with Specific Flags

HttpOnly cookies are inaccessible to JavaScript:

// Server sets cookie - JavaScript can NEVER read it
Set-Cookie: auth=session_token; HttpOnly; Secure; SameSite=Lax; Partitioned

Even if there’s an XSS vulnerability on your site, the attacker can’t steal the cookie.

// Server-side cookie setting
res.cookie('auth', sessionToken, {
    httpOnly: true,    // Prevents XSS theft
    secure: true,      // HTTPS only
    sameSite: 'lax',   // Allows subdomain navigation
    partitioned: true, // CHIPS: prevents tracking
    maxAge: 86400000   // 24 hours
});

Each flag serves a specific purpose:

Cookie FlagPreventsWhy It Matters
HttpOnlyXSS token theftMalicious scripts can’t read the cookie via JavaScript
SecureNetwork eavesdroppingCookie only sent over encrypted connections; prevents passive attackers from reading cookies on the wire
SameSite=LaxCSRF (partial)Blocks cookies in cross-site embedded contexts (like <iframe>), but allows top-level navigations
PartitionedCross-site trackingCookie is scoped to a partitioned jar, useless for fingerprinting across different top-level sites

Note on Secure: This flag protects against both active man-in-the-middle attacks and passive attackers who simply read traffic on the wire. Without it, cookies are sent in cleartext over HTTP.

SameSite: Strict vs. Lax

The SameSite attribute controls when cookies are sent with cross-site requests. Understanding the difference between Strict and Lax is critical.

SameSite=Strict: The browser never sends the cookie with any cross-site request. If you click a link to stellarwhiskers.com from Discord, your browser treats it as cross-site and refuses to send the cookie. You appear logged out until you refresh or click an internal link.

SameSite=Lax: The browser sends the cookie with “safe” top-level navigations (GET requests from links), but not with cross-site POST requests or embedded contexts (like <iframe>). This allows seamless navigation between subdomains while blocking most CSRF attacks.

Why Lax for Stellar Whiskers?

Stellar Whiskers uses multiple subdomains (accounts.stellarwhiskers.com for auth, games.stellarwhiskers.com for cloud save/load, and www.stellarwhiskers.com for the main site and game pages). These are considered cross-site by browsers, so SameSite=Strict would break navigation between them.

SameSite=Lax strikes the balance:

  • ✅ Allows cookies when navigating between subdomains
  • ✅ Blocks cookies in <iframe> embeds (prevents some CSRF)
  • ❌ Doesn’t prevent all cross-site requests (that’s where Partitioned helps)
  • With Lax (Better UX): If a player clicks your game link in Discord, the browser sees it as a “top-level GET navigation.” It sends the session cookie along, and the player lands on your site already logged in.
  • With Strict (Higher Friction): The browser refuses to send the cookie on that first click from Discord. The player appears logged out (even with an active session) until they refresh or click an internal link.

The Security Trade-off: Internal Trust

The catch with Lax (and even Strict) is that they operate at the site level (eTLD+1), not the origin level. Since accounts., games., and www. all share the same registrable domain (stellarwhiskers.com), the browser treats them as the same site.

This means if an attacker found a vulnerability on www.stellarwhiskers.com, they could potentially trigger a request to accounts.stellarwhiskers.com and the browser would send the cookie. For Stellar Whiskers, this is acceptable because:

  1. Full control: All subdomains are operated by the same team with consistent security practices
  2. No user-generated content: Unlike platforms like GitHub Pages (user1.github.io), we don’t host untrusted content on subdomains
  3. SameSite=Lax provides baseline CSRF protection: Cookies aren’t sent with most cross-site POST requests
  4. Origin validation: All state-changing endpoints validate the Origin header, blocking requests from unauthorized domains (defense-in-depth)

For a CMS platform or third-party hosting service where subdomains can’t trust each other, SameSite=Strict or per-subdomain cookies would be necessary. For a unified game platform like Stellar Whiskers, Lax provides the right balance of security and usability.

CHIPS: Partitioned Cookies in Action

The Partitioned flag (part of the CHIPS proposal—Cookies Having Independent Partitioned State) is the key innovation for privacy. Let me show you what this actually does with a concrete example.

Scenario: User visits evil-site.com which embeds stellarwhiskers.com in an iframe

1. evil-site.com loads <iframe src="https://stellarwhiskers.com/game">
2. Browser sends auth cookie to stellarwhiskers.com
3. stellarwhiskers.com can correlate this request with the user's activity on evil-site.com
4. This enables fingerprinting and tracking across sites
Scenario: User visits evil-site.com which embeds stellarwhiskers.com in an iframe

1. evil-site.com loads <iframe src="https://stellarwhiskers.com/game">
2. Browser sends auth cookie to stellarwhiskers.com, BUT it's stored in a partitioned jar
3. The partitioned cookie is scoped to (evil-site.com, stellarwhiskers.com) pair
4. stellarwhiskers.com cannot correlate this with the user's activity on other sites
5. The cookie is useless for tracking—the user is anonymous within this partition

Key insight: SameSite=Lax allows the cookie to be sent in navigational contexts (like clicking a link from Discord), but Partitioned ensures that even when the cookie is sent in embedded contexts, it can’t be used to track the user across different top-level sites.

This is critical for a privacy-first platform: we can authenticate users without building profiles that follow them across the web.

Authentication Flow

sequenceDiagram
    participant U as User
    participant C as Browser
    participant S as Auth Server
    participant G as Game Server

    U->>C: Enter credentials
    C->>S: POST /login
    S-->>C: Set-Cookie auth token
    C->>G: Join game with cookie
    G->>S: Validate session
    S-->>G: Valid user ID
    G-->>C: Game joined
    Note over C,S: Cookie never accessible to JavaScript

The game server never sees credentials—it only validates sessions with the auth server. And because the cookie is HttpOnly, even if there’s an XSS vulnerability in the game client, attackers can’t steal authentication.

What’s Next

Secure cookies solve the authentication problem—but what about protecting user data like email addresses and real names? When users sign in via Google or Apple, that PII needs to be encrypted so the server can’t read it even if compromised.

In Part 3: Asymmetric Encryption for Player PII, I’ll cover HPKE encryption, how the private key is isolated from the auth system, and why this transforms a “database breach” from a “total identity leak” into a “recovery nuisance.”