Networking in React — how the browser talks to servers, and how to secure it with HttpOnly cookies + CSRF tokens

1. What “networking” means in a React app (very short)

Networking = how your browser-based React app communicates with servers over HTTP(S). Typical flow:

  1. React triggers a request (fetch/axios).
  2. Browser sends HTTP request => server processes it => server sends HTTP response (usually JSON).
  3. React parses the response and updates UI.

Key building blocks: URLs, HTTP methods (GET/POST/PUT/PATCH/DELETE), headers, body, cookies, tokens, and CORS rules.

2. HTTP methods — when to use which

  • GET — read data (no body).
  • POST — create resource.
  • PUT — replace (full update).
  • PATCH — partial update.
  • DELETE — remove resource.

Always use the method that matches intent (helps caching, semantics, and security).

3. Headers & common headers you must know

  • Content-Type: what you send (e.g., application/json, multipart/form-data, text/html).
  • Accept: what you expect back.
  • Authorization: Bearer <token> for header tokens.
  • Cookie: browser auto-sends cookies for same-origin or when credentials included.
  • Custom header for CSRF: e.g. X-CSRF-TOKEN.

4. Cookies vs Token-in-header (short)

  • HttpOnly Cookie (recommended for sensitive web apps): stored by browser, not accessible from JS — protected from XSS, but you must defend against CSRF.
  • Token (localStorage/sessionStorage): easy to send in Authorization header, not automatically sent by browser — safe from CSRF but vulnerable to XSS.

Best modern recommendation (web apps): store authentication JWT in an HttpOnly cookie and protect write requests with a CSRF token.

5. CORS basics

When frontend origin ≠ backend origin, server must set CORS headers and allow credentials if you send cookies:

Server must include:

Access-Control-Allow-Origin: https://your.frontend.origin
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE
Access-Control-Allow-Headers: Content-Type, X-CSRF-TOKEN, Authorization        

On the client fetch:

fetch(url, { credentials: 'include' })        

6. The secure pattern: JWT in HttpOnly cookie + CSRF token

Why?

  • HttpOnly cookie => cannot be read by JS => protects from XSS.
  • But cookies are auto-sent => vulnerable to CSRF.
  • Add a CSRF token (random secret only frontend knows) that must be sent in a header on write requests — prevents CSRF.

High-level flow:

  1. Login => server sets access_token cookie (HttpOnly) and issues a csrf_token that frontend receives (via JSON or a non-HttpOnly cookie).
  2. Frontend stores csrf_token client-side (memory or localStorage).
  3. On POST/PUT/PATCH/DELETE, frontend sends X-CSRF-TOKEN: <csrf_token> header and credentials: 'include' so cookie is sent.
  4. Server checks header token matches expected token (cookie or server store) and verifies JWT from cookie.


7. Step-by-step implementation (Node.js/Express + React)

Two example approaches for sending the CSRF token to the client:

  • A. Send csrf_token as JSON in login response (client stores it).
  • B. Send csrf_token as a second cookie (non-HttpOnly) and also echo it in JSON. Both approaches possible — I’ll show approach A (clearer).

Backend (Node.js + Express) — minimal example

Install: npm i express cookie-parser cors jsonwebtoken

// server.js (simplified)
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';

const app = express();
app.use(express.json());
app.use(cookieParser());

// configure CORS to allow the frontend origin and credentials
app.use(cors({
  origin: 'http://localhost:3000', // your React app origin
  credentials: true
}));

const JWT_SECRET = process.env.JWT_SECRET || 'changeme';

// helper to generate secure CSRF token
function generateCsrfToken() {
  return crypto.randomBytes(32).toString('hex');
}

// LOGIN endpoint: verify credentials, set HttpOnly JWT cookie, return csrf token in body
app.post('/login', (req, res) => {
  const { email, password } = req.body;
  // --- validate credentials here ---
  const user = { id: 1, email }; // example
  const jwtPayload = { sub: user.id, email: user.email };

  const token = jwt.sign(jwtPayload, JWT_SECRET, { expiresIn: '15m' }); // short lived access token
  const csrfToken = generateCsrfToken();

  // Set HttpOnly access_token cookie
  res.cookie('access_token', token, {
    httpOnly: true,
    secure: true,       // set to true in production (HTTPS)
    sameSite: 'None',   // None if cross-site; Lax/Strict per app
    maxAge: 15 * 60 * 1000 // 15 minutes
  });

  // Optionally set a non-HttpOnly cookie with csrf so you can read it directly from cookie in client
  // res.cookie('csrf_token', csrfToken, { secure:true, sameSite:'None' });

  // Send CSRF to client in JSON (client stores it)
  res.json({ csrfToken });
});

// Protected endpoint example (requires CSRF match and valid JWT)
app.patch('/profile', (req, res) => {
  const csrfFromHeader = req.get('X-CSRF-TOKEN');
  // If you also stored csrf server-side (e.g., in redis/session), compare to that.
  // For demo, we expect the CSRF token to be sent in a second cookie or client-supplied header and matched server-side. 
  // If you used cookie 'csrf_token' you could compare req.cookies.csrf_token === csrfFromHeader.
  const csrfFromCookie = req.cookies['csrf_token']; // if you used cookie approach
  if (csrfFromCookie && csrfFromHeader !== csrfFromCookie) {
    return res.status(403).json({ message: 'Invalid CSRF token' });
  }
  // If you didn't store csrf in cookie, you would store csrf server-side for the session and compare.
  // Next: verify JWT from HttpOnly cookie
  const token = req.cookies['access_token'];
  if (!token) return res.status(401).json({ message: 'Unauthenticated' });

  try {
    const payload = jwt.verify(token, JWT_SECRET);
    // proceed with update...
    return res.json({ message: 'Profile updated', userId: payload.sub });
  } catch (err) {
    return res.status(401).json({ message: 'Invalid token' });
  }
});

app.listen(5000, () => console.log('Server running on 5000'));        

Notes:

  • Real apps should store server-session CSRF mapping (e.g., store the csrf token in a server-side store bound to session or user id) or issue the CSRF as a cookie you can validate. Otherwise an attacker could replay tokens — design depends on session state and your required level of security.
  • Use HTTPS (secure: true) in production.

Frontend (React + fetch)

Login — receive CSRF token and store it in memory/localStorage:

// auth.js
export async function login(email, password) {
  const res = await fetch('http://localhost:5000/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include', // important: allow server to set cookie
    body: JSON.stringify({ email, password })
  });
  const data = await res.json();
  // save CSRF token safely — in memory is best; localStorage is acceptable with caution
  localStorage.setItem('csrfToken', data.csrfToken);
}        

Send authenticated, CSRF-protected request:

export async function updateProfile(payload) {
  const csrf = localStorage.getItem('csrfToken'); // or from memory / react context
  const res = await fetch('http://localhost:5000/profile', {
    method: 'PATCH',
    credentials: 'include', // sends HttpOnly JWT cookie automatically
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-TOKEN': csrf
    },
    body: JSON.stringify(payload)
  });

  if (!res.ok) {
    // handle errors (401, 403, etc.)
    throw new Error('Request failed');
  }
  return res.json();
}        

Logout — clear CSRF token and tell backend to clear cookie:

export async function logout() {
  await fetch('http://localhost:5000/logout', {
    method: 'POST',
    credentials: 'include',
  });
  localStorage.removeItem('csrfToken');
}        

8. Where to store the CSRF token on the client

  • Memory (React state / context) — best for security (token cleared on reload but less persistent).
  • localStorage — persistent across reloads, easy to use but accessible to JS (XSS risk). If you already took care to prevent XSS, this is commonly used.
  • non-HttpOnly cookie — server can set csrf_token cookie (readable by JS) and client can simply read document.cookie (less recommended unless you know cookie behavior well). Recommendation: store CSRF in memory if possible; otherwise localStorage is acceptable.

9. CSRF token generation & validation patterns

  • Generate a cryptographically random token on login/session creation.
  • Bind the CSRF token to the user session or to the JWT ID so server can validate. (Either store it server-side per-session or send it in a cookie the server also can read.)
  • Validate: require that token in header equals token server expects (from session store, DB, or cookie).

10. Token expiry & refresh

  • Access tokens should be short-lived (e.g., 15 minutes).
  • Use refresh tokens to issue new access tokens. Refresh tokens may be stored in HttpOnly cookie and have stronger protections (rotate refresh tokens, use sameSite, secure flags).
  • On refresh, issue a new access_token cookie and a new csrf_token.

11. Common pitfalls & security checklist

  • Use HTTPS in production (secure: true cookie flag).
  • Use HttpOnly for access cookie.
  • Use SameSite (Lax/Strict/None) appropriately. (None + secure for cross-site).
  • Set Access-Control-Allow-Credentials: true and match Access-Control-Allow-Origin exactly (not *).
  • Verify CSRF token on all state-changing endpoints (POST/PUT/PATCH/DELETE).
  • Protect against XSS (sanitize inputs, Content Security Policy) — prevents token theft if you store things in localStorage.
  • Rotate tokens and set reasonable expirations.
  • Implement logout endpoint to clear cookies (Set-Cookie with expired date).

To view or add a comment, sign in

More articles by Kaumil Patel

Others also viewed

Explore content categories