
A comprehensive guide covering JWT structure, how they fit into OAuth 2.0 and OIDC, secure storage choices, refresh token rotation, XSS/CSRF risks, and scenarios where server‑side sessions are preferable.
What is a JWT and How Does It Work?
A JSON Web Token (JWT) is a compact, URL‑safe string that carries a set of claims between two parties, typically an authentication server and a client. The token consists of three Base64URL‑encoded parts separated by periods:
- Header – a JSON object that declares the token type (
typ) and the cryptographic algorithm (alg) used for signing or encryption. - Payload – the claim set, e.g., user identifier, roles, or expiration time. This is also a JSON object.
- Signature – the result of applying the algorithm from the header to the encoded header and payload, using a secret key (HMAC) or a private key (RSA/ECDSA).
When alg is set to none, the token contains only the header and payload, for example:
eyJhbGciOiJub25lIn0.eyJpZCI6IjEyMyIsIm5hbWUiOiJKb2huIn0
This “unsigned” form is insecure because anyone can modify the payload without detection; there is no cryptographic proof of integrity.
A signed JWT (a JSON Web Signature, JWS) adds the third component. Using HMAC SHA‑256 with a secret key your-256-bit-secret, a typical signed token looks like:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJKb2huIn0.4SkNQ2QZ8z5Lh7W0n2FK8KnXxXq_9yPmyMslK9YpN0A
The signature is generated by hashing the Base64URL‑encoded header and payload together, then encoding the hash. During verification, the server recomputes the hash with the same secret or public key and compares it to the received signature. If they differ, the token is rejected.
Why signatures matter for authentication:
- Integrity – Any alteration of claims (e.g., changing
rolefromusertoadmin) invalidates the signature. - Authenticity – Only parties possessing the signing key can produce a valid signature, establishing trust that the token originated from the authorized server.
- Stateless verification – APIs can validate a token without a database lookup, which aligns with compliance frameworks such as NIST SP 800‑63B that encourage strong, verifiable authentication mechanisms.
In practice, an authentication flow issues a signed JWT after credential verification, stores it (e.g., in localStorage or an HTTP‑only cookie), and presents it in the Authorization: Bearer header on subsequent requests. The server validates the signature on each request, ensuring that the claims have not been tampered with and that the request is truly authenticated.
JWTs in Modern Authentication Protocols
In OAuth 2.0 the token format is not prescribed, but JSON Web Tokens (JWTs) have become the de‑facto standard for access_token because they are self‑contained and can be validated locally without a network call to the authorization server. OpenID Connect (OIDC) builds an identity layer on top of OAuth 2.0 and mandates that the id_token be a JWT. The three tokens typically issued in an OIDC flow are:
- ID token (JWT) – contains claims that identify the authenticated user (e.g.,
sub,email,name). It is intended for the client application to read and should never be presented to a resource server. - Access token (often JWT) – authorizes calls to protected APIs. The API validates the token’s signature, expiration, and scopes. The token may be a JWT or an opaque string; when it is a JWT the claims can include
aud,scope, andexp. - Refresh token (opaque or JWT) – used only by the client to obtain new access tokens after the original expires. It is never sent to the API and should be stored securely (e.g., HTTP‑only cookie or secure server‑side storage).
Because the ID token is a JWT, developers sometimes mistake it for an access token and use it to protect API endpoints. This is a common security flaw: the API will accept a token that was never intended to convey authorization information, and the token may lack the required aud or scope claims. The correct pattern is to validate the access_token on every request and keep the ID token confined to the client’s authentication logic.
Practical example
// Client receives tokens from the OIDC token endpoint
const { id_token, access_token, refresh_token } = response;
// Store refresh token in an HTTP‑only cookie
document.cookie = `refresh=${refresh_token}; HttpOnly; Secure; SameSite=Strict`;
// Use access token for API calls
fetch('/api/orders', {
headers: { Authorization: `Bearer ${access_token}` }
});
When the access token expires, the client sends the refresh token to the token endpoint to obtain a new access_token. This rotation reduces the window of exposure if an access token is compromised and aligns with best practices described in OWASP’s authentication cheat sheet and NIST SP 800‑63B.
Secure Storage and Token Lifecycle Management
JSON Web Tokens (JWTs) are signed strings that convey authentication claims. Because the signature can be verified without a server‑side lookup, they are often stored client‑side for subsequent API calls. The storage mechanism determines the attack surface for two primary web threats: cross‑site scripting (XSS) and cross‑site request forgery (CSRF).
XSS impact – any script that runs in the browser can read data from localStorage or sessionStorage. If an attacker injects malicious JavaScript, a token stored there can be exfiltrated and used elsewhere. CSRF impact – cookies are automatically attached to every request to the token’s domain. An attacker who can lure a user’s browser into sending a forged request can cause the server to accept a valid token, unless the cookie is protected with SameSite and CSRF defenses.
Because of these differences, the choice of storage should follow the principle “store where it is least exposed to the relevant threat.”
- httpOnly, Secure, SameSite=Strict/Lax cookies – not accessible to JavaScript, mitigating XSS. Proper
SameSitesettings reduce CSRF risk. Ideal for short‑lived access tokens. - Memory (e.g., a JavaScript variable) – token exists only for the page lifecycle, eliminating persistent XSS exposure. Requires re‑authentication on reload, which can be mitigated by a rotation‑based refresh token.
- localStorage / sessionStorage – convenient but fully readable by JavaScript, making them unsuitable for high‑value tokens unless the application is hardened against XSS (e.g., CSP, input sanitisation per OWASP ASVS).
For refresh tokens, rotation is recommended: each time a refresh request succeeds, the server issues a new refresh token and invalidates the previous one. This limits the window of abuse if a token is compromised. A typical flow:
fetch('/auth/refresh', {
method: 'POST',
credentials: 'include' // send httpOnly cookie
}).then(r => r.json())
.then(data => {
// store new access token in memory
window.accessToken = data.accessToken;
});
Token expiration should be short for access tokens (minutes) and longer for refresh tokens (days). Short lifetimes reduce the impact of a stolen token, while rotation ensures that a stolen refresh token becomes unusable after its first use.
Aligning these practices with compliance frameworks (e.g., NIST SP 800‑63B, ISO 27001, SOC 2) satisfies requirements for credential protection, secure session management, and auditability of token issuance and revocation.
Limitations and Situations to Avoid JWTs
JSON Web Tokens (JWTs) are compact, URL‑safe strings that carry signed claims. While their self‑contained nature simplifies stateless authentication, several intrinsic constraints make them unsuitable for certain enterprise scenarios.
- Size and bandwidth impact – A JWT includes a header, payload, and signature, each base64url‑encoded. Adding custom claims (e.g., roles, permissions, or user preferences) inflates the token. Large tokens increase request‑header size, can exceed limits of some proxies or load balancers, and degrade performance for high‑throughput APIs.
- No built‑in confidentiality – The JWT specification defines signing (JWS) but not encryption (JWE) by default. Unless the token is explicitly encrypted, its payload is readable by any party that can intercept the HTTP request. Sensitive data such as personally identifiable information (PII) must therefore be omitted or protected with an additional encryption layer.
- Revocation complexity – Because JWTs are stateless, the server does not retain a reference to each issued token. Revoking a token before its expiration requires either short lifetimes with frequent refreshes or a server‑side blacklist, both of which reintroduce state and can conflict with the original purpose of JWTs.
- Inadequate for fine‑grained session control – Scenarios that demand immediate session termination (e.g., after password change, suspicious activity, or regulatory logout) are better served by server‑side sessions where the session identifier can be invalidated instantly.
When compliance frameworks such as SOC 2, ISO 27001, or NIST require strict auditability and revocation capabilities, server‑side session stores align more naturally with the required controls. The OWASP Top Ten also highlights token exposure and insufficient expiration handling as common risks.
Practical guidance:
- Use JWTs only for short‑lived access tokens (minutes to hours) that contain non‑sensitive claims.
- Store refresh tokens securely on the server or in an HttpOnly cookie; never expose them to client‑side JavaScript.
- Prefer server‑side sessions for applications that need immediate logout, per‑device revocation, or extensive audit trails.
- If confidentiality is required, combine JWT signing with JWE encryption or encrypt the payload before embedding it in the token.
Choosing Between JWTs and Server‑Side Sessions
JSON Web Tokens (JWTs) are signed, self‑contained strings that convey claims about a user without requiring a server‑side lookup on each request. A typical signed JWT consists of a base64url‑encoded header, payload, and signature (e.g., eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJhZ2UiOjM2fQ.4SkNQ2QZ8z5Lh7W0n2FK8KnXxXq_9yPmyMslK9YpN0A). Because the signature can be verified with a secret or public key, the token’s integrity is guaranteed, allowing stateless authentication across distributed services.
Traditional server‑side sessions store a session identifier (often in a cookie) and keep the associated user state in memory, a database, or a cache. The server validates the identifier on each request and can instantly revoke a session by deleting its entry.
Selection Criteria
- Scalability & statelessness: JWTs eliminate the need for shared session stores, which benefits horizontally scaled microservices.
- Revocation & logout: Server‑side sessions allow immediate invalidation; JWT revocation requires short lifetimes, refresh‑token rotation, or a token blacklist.
- Payload size: JWTs carry claims in every request, increasing header size; sessions keep only a small identifier.
- Security surface: Storing JWTs in
localStorageorsessionStorageexposes them to XSS, while cookies withHttpOnlyandSameSiteflags mitigate XSS/CSRF risks per OWASP recommendations. - Compliance requirements: Standards such as SOC 2, ISO 27001, and NIST SP 800‑63‑3 demand strong session management and auditability; server‑side sessions simplify logging and expiration control.
Actionable Recommendations
- Use JWTs when you need stateless authentication across multiple services and can tolerate short‑lived access tokens (e.g., 15 minutes) combined with securely stored refresh tokens.
- Prefer server‑side sessions for applications that require immediate revocation, fine‑grained audit trails, or where compliance audits demand explicit session lifecycle records.
- If you choose JWTs, store them in
HttpOnlycookies to reduce XSS exposure, and implement refresh‑token rotation to mitigate token theft. - When using server‑side sessions, back the session store with a resilient cache (e.g., Redis) and enforce idle‑timeout and absolute expiration aligned with your security policy.
- Validate JWT signatures on every request, check the
expclaim, and reject tokens with mismatched audience (aud) or issuer (iss) values.
In practice, a microservice API might accept a JWT in the Authorization: Bearer header, verify its signature, and extract the user ID from the sub claim. Conversely, a monolithic web app could rely on a session cookie, look up the user record in a Redis store, and instantly invalidate the session on logout. Choosing the appropriate mechanism hinges on the trade‑offs outlined above.
Looking for Custom Software or AI Solutions?
Appworks Technologies designs, builds, and scales production enterprise platforms, microservices, and AI agent workflows tailored to your business goals.
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.
