How JWTs Work and Where to Store Them Safely
JSON Web Tokens (JWTs) are a compact, URL-safe standard used to securely transmit data between clients and servers for authentication and authorization. This guide breaks down the internal structure and mechanics of JWTs, outlines the standard authentication workflow, and details the most secure storage patterns within JavaScript web applications to protect your users against common vulnerabilities like Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).
Understanding the Structure of a JWT
A JSON Web Token is a single string composed of three distinct parts
separated by periods (.):
- Header: Contains metadata about the token,
typically the type of token (
JWT) and the signing algorithm used (such asHS256orRS256). - Payload: Contains the claims, which are statements
about the user and additional context (e.g.,
userId,role, and token expiration timestampexp). This data is Base64URL-encoded, meaning it is readable by anyone who inspects the token and must never contain sensitive secrets like plain passwords. - Signature: Created by taking the encoded header, the encoded payload, and hashing them with a secret key (or private key) using the algorithm specified in the header.
Because the signature is verified against the payload, any tampering with the claims automatically invalidates the token.
The JWT Authentication Workflow
- Authentication: The user submits their credentials (e.g., email and password) to the authentication server.
- Generation: Upon successful validation, the server generates a JWT containing the user’s identity claims, signs it with a private key or secret, and sends it back to the client.
- Transmission: For subsequent protected API
requests, the client sends this token to the server, typically inside
the
Authorizationrequest header using theBearer <token>schema. - Verification: The server verifies the token’s cryptographic signature using its secret or public key. If the signature is valid and the token is not expired, the request is processed without requiring a database lookup for session data.
Where to Safely Store JWTs in JavaScript Web Apps
Choosing where to store tokens in the browser represents a critical security trade-off between XSS vulnerability and CSRF vulnerability.
1. Browser Storage
(localStorage / sessionStorage)
- Vulnerability: Highly susceptible to XSS.
- Why to avoid: Any JavaScript code executing on your
domain—including third-party scripts, analytics tools, or compromised
npm packages—can access
window.localStorageand extract the token. Storing tokens here makes account takeover trivial once an XSS vulnerability exists.
2. In-Memory (JavaScript Variable)
- Vulnerability: Immune to CSRF and persistent XSS extraction, but lost on page refresh.
- How it works: Storing the access token in memory (such as a plain variable, a Redux store, or React state) prevents malicious scripts from pulling it out of persistent storage. However, the token disappears whenever the user refreshes or navigates away.
3. HttpOnly,
Secure Cookies (Recommended)
- Vulnerability: Protected from XSS token theft; requires CSRF mitigation.
- How it works: When the server issues the token, it
sets it via the
Set-Cookieresponse header with the following flags:HttpOnly: Blocks client-side JavaScript from accessingdocument.cookie, neutralizing token theft via XSS.Secure: Ensures the cookie is only transmitted over encrypted HTTPS connections.SameSite=StrictorSameSite=Lax: Restricts when cookies are sent with cross-site requests, mitigating CSRF attacks.
The Best-Practice Architecture: Dual-Token Pattern
The most secure approach for modern JavaScript single-page applications (SPAs) is the dual-token system:
- Short-Lived Access Token (Stored in Memory): Used to authenticate individual API requests. It has a short lifespan (e.g., 5 to 15 minutes) and lives entirely in JavaScript memory.
- Long-Lived Refresh Token (Stored in an
HttpOnly,Secure,SameSiteCookie): Has a longer lifespan (e.g., 7 days) and cannot be read by JavaScript. - Silent Refresh Cycle: When the in-memory access
token expires, the client calls a dedicated
/refresh-tokenendpoint. The browser automatically attaches the secure refresh token cookie. The server validates the cookie, issues a new short-lived access token, and returns it to the client memory, maintaining a seamless and secure session.