
Explore the fundamentals of JSON Web Tokens, how they fit into OAuth 2.0 and OpenID Connect, secure storage strategies, and key security considerations. Learn when JWTs shine and when traditional server‑side sessions are a better fit.
What is a JWT?
JSON Web Token (JWT) is a compact, URL‑safe representation of a set of claims that can be transmitted between two parties, typically a server and a client. The token is a single string composed of three Base64URL‑encoded segments separated by periods (.). The three segments are:
- Header – a JSON object that describes the token type (usually
"typ":"JWT") and the cryptographic algorithm used to secure the token (e.g.,"alg":"HS256"for HMAC‑SHA‑256). The header is Base64URL‑encoded to form the first part of the token. - Payload (claims) – a JSON object containing statements about an entity (often a user) and additional metadata. Common claim names include
sub,exp,iat, as well as application‑specific fields such asid,name, androle. This JSON is also Base64URL‑encoded to become the second part. - Signature – a cryptographic value generated by applying the algorithm declared in the header to the concatenated Base64URL‑encoded header and payload, using a secret key (for symmetric algorithms) or a private key (for asymmetric algorithms). The resulting binary signature is Base64URL‑encoded and appended as the third segment.
Example of a signed JWT (HS256):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ
.4SkNQ2QZ8z5Lh7W0n2FK8KnXxXq_9yPmyMslK9YpN0A
The same structure can be used for an unsigned token, where the header’s alg value is "none". In that case the token consists only of the header and payload, with no third segment. Because there is no cryptographic proof of integrity, an unsigned token cannot be trusted for authentication or authorization; any party can modify the payload without detection.
Signed tokens (JSON Web Signatures, JWS) provide integrity and, when using asymmetric keys, non‑repudiation. Verification requires the recipient to recompute the signature using the appropriate key and compare it to the token’s third segment. If the signatures match, the claims are guaranteed to be unchanged since issuance.
In practice, authentication systems store the signed JWT (often in localStorage or an HTTP‑only cookie) and present it on each request. The server validates the signature and extracts the claims, eliminating the need for repeated database lookups while ensuring that tampering is cryptographically infeasible.
How JWTs Are Used in Authentication
In a typical JWT‑based login flow the client authenticates once with an authorization server, receives a signed JSON Web Token (JWS), stores it, and presents it on every request to a protected resource. The process can be broken down into four phases:
- Credential submission. The user posts a username and password (or an OIDC‑compatible credential) to
/auth/login. The server validates the credentials against its identity store. - Token issuance. Upon successful authentication the server creates a JWT whose header declares the signing algorithm (e.g.,
"alg":"HS256"), whose payload contains claims such assub(user ID),name, and anexptimestamp, and whose signature is generated with a secret key known only to the server. The resulting string has the formheader.payload.signature, for example:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIERvZSIsImV4cCI6MTY5MzU5MDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Secure storage. The client stores the token in a location that is not automatically sent with every request. Common choices are
localStorageorsessionStoragefor single‑page applications, or an HttpOnly, Secure cookie for server‑rendered sites. The storage decision influences the attack surface: storage in JavaScript‑accessible locations is vulnerable to XSS, while cookies mitigate XSS but can be targeted by CSRF if not protected with theSameSiteattribute. - Presentation to APIs. For each call to a protected endpoint, the client reads the token from storage and adds it to the
Authorizationheader using the Bearer scheme:Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
The resource server validates the signature with the shared secret or public key, checks theexpclaim, and extracts the user claims to enforce authorization decisions.
When the access token expires, a separate refresh token—typically an opaque string or a JWT with a longer lifespan—is sent to a dedicated /auth/refresh endpoint to obtain a new access token without re‑prompting the user. Refresh tokens should be stored more securely (e.g., HttpOnly cookies) and rotated after each use to limit replay risk, a practice recommended by OWASP and NIST guidelines for token management.
JWTs in OAuth 2.0 and OpenID Connect
In OAuth 2.0 the authorization server issues an access token that a client presents to a protected API. The specification does not prescribe a token format, but JSON Web Tokens (JWTs) have become the de‑facto standard because they are self‑contained: the token carries the required claims and a cryptographic signature that can be verified without a network call to the issuer.
OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0. OIDC always returns an ID token, which is a JWT that contains authentication‑related claims such as sub, email, and name. The ID token is intended for the client application to establish “who the user is”; it is not meant to be presented to resource servers.
A typical OIDC flow therefore returns three tokens:
- ID token – a JWT with user identity claims; consumed only by the client.
- Access token – often a JWT, used by the client to call APIs; the API validates the signature and the scopes contained in the token.
- Refresh token – opaque or JWT, used to obtain new access tokens; never sent to the API.
Because both token types share the JWT structure, developers sometimes conflate their purposes. A common pitfall is using the ID token for API authorization. An API that validates an ID token will be checking claims that were never intended for resource access (e.g., iss = Google, aud = client‑id). This can lead to:
- Incorrect scope enforcement, since ID tokens do not carry OAuth scopes.
- Increased attack surface if the client leaks the ID token to a third‑party API.
- Confusion in token revocation, because refresh‑token rotation typically applies only to access tokens.
Practical guidance:
- Validate the
audclaim against the API’s identifier, not the client’s. - Check the
scopeor custom resource‑access claims in the access token before granting permission. - Store refresh tokens securely (e.g., HttpOnly cookies) and rotate them on each use.
When implementing OIDC, treat the ID token as a signed assertion for the client’s session state, and treat the access token as the credential for API calls. This separation aligns with the OAuth 2.0 authorization framework and the OIDC identity layer, and it prevents the misuse of identity information for authorization decisions.
Secure Storage and Token Lifecycle
In a JWT‑based flow the client receives an access token (short‑lived) and a refresh token (long‑lived). The access token is presented to APIs, while the refresh token is used only with the authorization server to obtain a new access token. Understanding the attack surface of each storage location is the first step toward a secure implementation.
Storage location trade‑offs
Tokens stored in localStorage or sessionStorage are accessible to any JavaScript running in the origin. This makes them vulnerable to cross‑site scripting (XSS) attacks, a risk highlighted by OWASP’s Top‑10. Conversely, httpOnly cookies are not exposed to JavaScript, mitigating XSS, but they are automatically sent with every request to the cookie’s domain, which can be exploited by cross‑site request forgery (CSRF) unless additional defenses (SameSite attribute, anti‑CSRF tokens) are applied.
- httpOnly + SameSite=Strict/Lax: protects against XSS and reduces CSRF exposure.
- Secure flag: ensures the cookie is transmitted only over TLS, satisfying NIST SP 800‑63B transport requirements.
- localStorage: acceptable only when the application enforces a strict Content‑Security‑Policy (CSP) and performs thorough input sanitisation, but this is rarely sufficient for high‑value enterprise data.
Refresh token rotation
Refresh token rotation replaces the used token with a newly issued one on each renewal request. If an attacker steals a refresh token, the legitimate client will detect the reuse because the server will reject the stale token. This aligns with SOC 2 and ISO 27001 controls that require detection of credential misuse.
POST /token/refresh
{
"refresh_token": "oldRefreshToken"
}
The server validates the token, issues a new access token and a new refresh token, and revokes the old refresh token.
Access token expiration and renewal flow
Access tokens should have a brief lifespan (e.g., 5–15 minutes). When an API returns 401 Unauthorized due to expiration, the client initiates a silent renewal using the stored refresh token. A typical pattern is:
- Detect
401response. - Send a refresh request with the httpOnly cookie containing the refresh token.
- Replace the in‑memory access token with the new value.
- Retry the original request.
Implementing the renewal logic in a central HTTP interceptor (e.g., Axios or Fetch wrapper) ensures consistent handling across the codebase and reduces the chance of accidental token leakage.
Security Risks: XSS, CSRF, and JWT Limitations
The security posture of an application using JSON Web Tokens (JWT) is fundamentally tied to storage architecture and the inherent limitations of the format. Engineering teams must balance the convenience of client-side storage against the risks of Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).
Storage and Attack Surfaces
Where you store a JWT directly determines which vulnerability vector poses the greatest threat to your application:
- Browser Storage (LocalStorage/SessionStorage): Tokens stored here are accessible to any JavaScript running on the page. If the application is vulnerable to XSS, an attacker can programmatically extract the JWT, leading to session hijacking. Because these storage mechanisms are not automatically sent with HTTP requests, they are generally immune to CSRF.
- HTTP-Only Cookies: Storing tokens in HTTP-only cookies prevents JavaScript access, effectively mitigating token theft via XSS. However, this shifts the risk toward CSRF. Unless strictly controlled via
SameSitecookie attributes and anti-CSRF tokens, browsers will automatically append these cookies to cross-origin requests.
JWT Limitations and Misconceptions
JWTs are frequently misunderstood regarding their size and security guarantees. Developers should account for the following constraints:
- Size Constraints: JWTs are not intended to transport large datasets. As tokens increase in size, they can exceed header length limits enforced by web servers (e.g., Nginx or Apache) or cause performance degradation due to the overhead added to every HTTP request.
- Encryption vs. Signing: A common misconception is that all JWTs are encrypted. By default, many implementations use JSON Web Signatures (JWS), which ensure integrity (preventing tampering) but not confidentiality (hiding content). Anyone with access to the base64url-encoded string can decode and read the payload. If sensitive data must be hidden, you must implement JSON Web Encryption (JWE).
- Statelessness Trade-offs: While JWTs eliminate the need for database lookups during request validation, they make session revocation difficult. Without a stateful blacklist, a stolen token remains valid until its expiration time, limiting the effectiveness of immediate account deactivation.
To ensure robust security, treat JWTs as transient credentials. Always validate signatures on the server side using the appropriate algorithm, and avoid relying on tokens with excessively long expiration periods, which expand the window of opportunity for misuse.
When to Choose JWTs vs. Server‑Side Sessions
JSON Web Tokens (JWTs) are signed, self‑contained strings that carry a set of claims. Because the signature can be verified with a shared secret or public key, a resource server can trust the token without contacting the issuer on each request. Server‑side sessions, by contrast, store a minimal identifier (often a random session ID) in a cookie while the full user state lives in a server‑side store such as Redis or a relational database.
When JWTs are the appropriate choice
- Stateless microservice architectures where each service must validate a token without a network hop to a central session store.
- Cross‑origin or mobile clients that need a portable token for API calls; the token can be placed in the
Authorization: Bearerheader. - Scenarios requiring reduced database load: claims (e.g., user role, tenant ID) are embedded in the token, allowing the API to authorize without a lookup.
- Integration with OAuth 2.0 or OpenID Connect (OIDC) flows, where access tokens and ID tokens are defined as JWTs.
Example: an API gateway validates a JWT signed with HS256, extracts the role claim, and forwards the request to downstream services without any session lookup.
When server‑side sessions provide stronger security or simplicity
- When immediate revocation is required; deleting the session entry instantly invalidates the user’s access, whereas a JWT remains valid until its expiration.
- When sensitive data must never travel to the client; storing it server‑side eliminates exposure even if a token is intercepted.
- When compliance frameworks such as SOC 2, ISO 27001, or NIST SP 800‑63 require strict control over authentication state and audit trails; a central session store simplifies logging and access‑control reviews.
- When the application already uses a mature session middleware (e.g.,
express-session) and the added complexity of token rotation, refresh‑token storage, and XSS mitigation (as recommended by OWASP) outweighs the benefits.
Example: a traditional web app stores a session ID in an HttpOnly cookie. The server invalidates the session on logout, and the short‑lived cookie mitigates CSRF when combined with same‑site attributes.
In practice, many teams adopt a hybrid approach: use short‑lived JWTs for API authentication while maintaining server‑side sessions for interactive web pages that benefit from built‑in CSRF protection and easy revocation. The decision should be driven by the threat model, compliance obligations, and operational overhead rather than by the perceived “modernity” of JWTs.
Editorial Policy & Research Methodology
Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.
