Post banner

React Authentication - Part 1: Storing JWT Tokens Securely

7 min read
Table of Contents

Note: This is a series of posts divided into the following parts:

  • React Authentication - Part 1: Storing JWT Tokens Securely [THIS POST]
  • React Authentication - Part 2: Structuring a Scalable React App - not yet published
  • React Authentication - Part 3: Building the User Interface - not yet published
  • React Authentication - Part 4: Building the Authentication Logics - not yet published

Background Context

Back in 2020, when I first learned React, I followed multiple YouTube tutorials on JWT authentication. At the time, most online tutors recommended storing JWT tokens in localStorage.

As a beginner, I didn’t question it—I just followed along. That’s how I implemented authentication in many of my early React projects.

But as I gained more experience, I noticed a growing number of developers criticizing the practice of storing JWT tokens in localStorage.

Many argued that it was insecure and should never be used in a production-grade application. This left me confused.

After diving deeper into the topic, I discovered that JWT tokens can actually be stored in several ways:

  • localStorage
  • sessionStorage
  • cookies
  • In-memory storage

Each method has its pros and cons, and many articles and videos break them down in detail. Instead of repeating those explanations here, I recommend checking out these resources:

Storing JWTs in localStorage Problem

I started questioning why so many developers strongly advised against using localStorage for JWT tokens. Here’s what I found:

Vulnerable to XSS (Cross-Site Scripting) Attacks

  • If an attacker injects malicious JavaScript into your app, they can easily get tokens stored in localStorage and steal them.
  • This is a significant risk, especially in Single Page Applications (SPAs) where JavaScript runs client-side.

XSS example

Harder to Securely Manage Token Expiry & Refreshing

  • Since access tokens expire, you need a secure way to refresh them.
  • A poorly implemented refresh mechanism can expose vulnerabilities, such as leaking refresh tokens in the client. Which gives attacker the ability to keep extending their access to our system.

So, if localStorage is insecure, what’s the best way to store JWTs in a production-grade React application?

Storing JWT Tokens Securely

One of the most secure and efficient way to handle JWT authentication in a React application is to store:

  • Access Tokens in in-memory storage or state
  • Refresh Tokens in HttpOnly cookies

Storing JWT Tokens

Access Token in In-Memory Storage

  • Protected from XSS attacks – Unlike localStorage, in-memory storage (such as React state or a global store like Valtio) isn’t accessible by malicious scripts.
  • Short-lived – Access tokens expire quickly, reducing the damage if compromised.

Data in browser

Refresh Token in HttpOnly Cookies

  • Completely hidden from JavaScript – Since HttpOnly cookies can’t be accessed by JavaScript, they’re immune to XSS attacks.
  • Automatic token handling – Cookies are automatically sent with every request, making authentication seamless.
  • Secure session management – You can configure Secure, SameSite, and HttpOnly attributes for added protection.

With this setup, even if an attacker injects malicious JavaScript, they won’t be able to steal tokens, making your authentication flow much more secure.

Understanding JWT Tokens

Before diving into implementations in React, let’s understand JWT Tokens and their roles in authentication.

What is JWT?

JWT (JSON Web Token) is a compact, self-contained format for securely transmitting information between parties. It’s commonly used for authentication in web applications. A JWT consists of three parts:

  1. Header – Contains metadata, like the signing algorithm.
  2. Payload – Contains claims such as user ID, role, and expiration time.
  3. Signature – Ensures the token hasn’t been tampered with.

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6InVzZXIiLCJleHAiOjE3MDAwMDAwMDB9.WCQlBfIq6qAgAXnVxPQJZ1DPCqHP0dxnV9IvqFl07KQ

When decoded, the payload contains structured data:

{
  "userId": 123,
  "role": "user",
  "exp": 1700000000
}

This token represents a user with ID 123, assigned the role of "user", and an expiration timestamp. The backend can verify this token and use its data to grant access to protected resources.


Access Tokens

An Access Token is a short-lived JWT that the client uses to authenticate requests. When a user logs in, the server generates an Access Token and sends it to the client, which then includes it in the Authorization header when making API calls.

Access Token

Since Access Tokens have a short lifespan (usually 15 minutes to an hour), they limit the risk of stolen tokens being used indefinitely. However, when an Access Token expires, the user would be logged out unless there’s a way to refresh it.


Refresh Tokens

To avoid forcing users to log in repeatedly, our authentication systems can use Refresh Tokens. These are long-lived tokens that allow the client to obtain a new Access Token without requiring the user to enter credentials again.

Example: Refreshing an Expired Access Token

  1. The user logs in, and the backend issues both an Access Token and a Refresh Token.
  2. When the Access Token expires, the client sends a request to the /api/refresh-token endpoint, using the Refresh Token stored in cookies.
  3. The backend verifies the Refresh Token and issues a new Access Token.

What’s next?

Now that we understand the problems with localStorage and why HttpOnly cookies & in-memory storage are the better alternatives, let’s move on to implementing JWT authentication the right way.

In the upcoming articles, we’ll implement a production-grade authentication in React App using:

  • Modern Tech Stack 🛠️

    • ⚡ Vite + TypeScript for type-safe development
    • 🏪 Valtio for state management
    • 🔄 React Query for data fetching
    • ✅ Zod for runtime type validation
    • 🛣️ React Router for navigation
    • 📝 React Hook Form for form validation
    • 🎨 Tailwind CSS for styling
  • Modern Authentication Flow 🔐

    • 🎫 JWT Authentication
    • 💾 Access token in in-memory storage
    • 🍪 Refresh token in HttpOnly cookies
    • 🛡️ Secure token management
    • 🔄 Automatic access token refresh
    • 👋 Secure User Logout

Here’s what we’ll cover:

  1. Setting Up Our React Application

    • Modern project setup with Vite and TypeScript
    • Configuring essential development tools (ESLint, Prettier, etc)
    • Implementing MVVM architecture for clean separation of concerns
  2. Building Authentication Logic

    • Implementing secure token management
    • Creating type-safe HTTP client with automatic token refresh
    • Setting up protected routes and authentication hooks
  3. Creating the User Interface

    • Building login/signup forms with validation
    • Implementing secure authentication flows
    • Handling error states

Let’s get started!