Understanding JSON Web Tokens (JWT): Structure, Security, and Best Practices
Demystify JSON Web Tokens (JWT). Learn how they work under the hood, explore their structural anatomy, avoid common security pitfalls, and discover how to debug them safely.
In modern web development, securing user sessions and managing stateless authentication is a core engineering requirement. Traditional session-based authentication relies on storing session identifiers on a server database or cache. However, as applications scale to distributed architectures, microservices, and serverless environments, stateless authentication has surged in popularity.
At the heart of stateless authentication lies the JSON Web Token (JWT), defined by RFC 7519. JWTs allow parties to transmit information as a self-contained JSON object. While a JWT can be digitally signed to allow its integrity to be checked, whether this signature actually establishes authenticity depends entirely on the chosen signing model, key management approach, and verification discipline. Crucially, standard signed JWTs (JSON Web Signatures, or JWS) do not provide confidentiality; they protect against tampering, but they do not encrypt the payload. To ensure confidentiality, encryption mechanisms such as JSON Web Encryption (JWE) must be used instead. Because a properly signed token is digitally verifiable, the receiver can verify its integrity and origin without necessarily performing a database lookup solely for token validation. However, downstream authorization decisions must still be strictly enforced server-side, and applications may still query databases or caches for user status, authorization details, token revocation checks (such as blocklists), or refresh token management.
In this guide, we will dissect the anatomy of a JWT, explore how signatures protect integrity and authenticity, review critical security pitfalls, and learn how to debug them safely.
1. The Anatomy of a JWT
A JSON Web Token consists of three distinct parts separated by period (.) characters:
- Header
- Payload
- Signature
In its serialized form, a JWT looks like a single string of characters:
xxxxx.yyyyy.zzzzz
Let’s break down each of these three sections.
The Header
The header typically consists of two parts: the type of the token (which is JWT) and the signing algorithm being used, such as HMAC SHA256 (HS256) or RSA SHA256 (RS256). JWT headers may also contain additional fields such as kid (Key ID), which is commonly used for key selection during verification and key rotation. Note that the typ header (e.g., typ: "JWT") is purely informational to help dispatchers identify the token type, and should not be treated as a security control or verification guard by the receiving application.
{
"alg": "HS256",
"typ": "JWT",
"kid": "key-id-1"
}
This JSON is then Base64URL encoded to form the first part of the JWT.
The Payload
The payload contains the claims. Claims are statements about an entity (typically, the user) and additional metadata. There are three types of claims:
- Registered claims: A set of predefined, recommended claims defined by the spec (e.g.,
issfor issuer,subfor subject,audfor audience,expfor expiration time, andiatfor issued at time). - Public claims: Custom claims designed to be collision-resistant, often defined in public registries or using URIs.
- Private claims: Custom claims shared between parties to pass information (e.g., user roles, application permissions, or account identifiers).
{
"sub": "usr_987654321",
"roles": ["admin", "billing"],
"scope": "read:profile write:reports",
"iat": 1718921600,
"exp": 1718925200
}
The payload JSON is then Base64URL encoded to form the second part of the JWT. It is crucial to understand that JWT payloads are Base64URL-encoded but not encrypted, unless JSON Web Encryption (JWE) is explicitly used. Sensitive information (such as passwords, credit card numbers, or internal API keys) should never be stored in JWT claims because anyone who intercepts the token can decode it instantly.
The Signature
To create the signature part, you take the encoded header, the encoded payload, a secret key, and the algorithm specified in the header, sign them, and then Base64URL-encode the resulting HMAC or signature output.
For example, if you are using the HMAC SHA256 algorithm, the signature is created as follows:
// Conceptual representation of signature generation
const signature = base64UrlEncode(HMAC_SHA256(
`${encodedHeader}.${encodedPayload}`,
secretKey
));
The signature is used to verify that the message wasn’t changed along the way. In the case of tokens signed with a private key (like RS256), it proves that the token was issued by the holder of that private key and has not been modified.
2. Base64 vs. Base64URL Encoding
A common point of confusion is how JWT strings are encoded. JWTs use Base64URL encoding rather than standard Base64 encoding.
Standard Base64 encoding includes the characters + and /, and uses = for padding at the end. These characters can cause issues in URL contexts; for example, / separates path segments, and + may be interpreted as a space in URL-encoded form data.
To make JWTs safe to transmit inside HTTP headers, cookie strings, and query parameters, Base64URL encoding modifies standard Base64 by:
- Replacing
+with-(minus) - Replacing
/with_(underscore) - Omitting the trailing padding character
=
It is important to remember that Base64URL encoding is not encryption. Anyone who intercepts a JWT can easily decode the header and payload to view their contents. Keep in mind that you should never store sensitive data (like passwords, API keys, or credit card numbers) in a JWT payload.
3. How Signatures Ensure Security
Signatures are fundamental to JWT security because they provide integrity protection and authenticity. While the header and payload can be read by anyone, they cannot be altered without breaking the cryptographic signature. However, the overall security of a JWT-based system does not rely on the signature alone; it is a multi-layered model that also depends on thorough claim validation, strict algorithm restrictions, robust key management, and secure token storage.
There are two primary paradigms for signing JWTs:
Symmetric Signing (e.g., HS256)
Symmetric algorithms use a single shared secret to both sign and verify the token. Both the authentication server (which issues the token) and the API server (which consumes and verifies the token) must possess the exact same secret key. This is simple and fast, but if multiple microservices need to verify tokens, distributing the shared secret securely to all of them can be challenging.
Asymmetric Signing (e.g., RS256, ES256, or EdDSA)
Asymmetric algorithms use a public/private key pair. Popular options include RS256, ES256, or EdDSA (Ed25519), a modern signing algorithm supported by many JWT libraries for its high performance and security profile. The authentication server signs the token using a highly guarded private key. Anyone else (such as client apps, third-party services, or internal microservices) can verify the token’s integrity using the corresponding public key.
The public key can be safely shared over a public endpoint (such as a JSON Web Key Set, or JWKS endpoint). In this setup, the header’s kid (Key ID) claim identifies which key in the JWKS was used to sign the token. Rather than fetching keys from arbitrary, untrusted, or attacker-controlled locations, verifiers must use the kid to look up the public key exclusively from a pre-configured, trusted JWKS endpoint. To optimize performance, secure the application from rate-limiting issues, and prevent key retrieval denial-of-service vulnerabilities, verifiers should cache the retrieved keys locally using a secure cache-management strategy. This caching mechanism, coupled with a secure policy for lazy-loading missing keys when a new, trusted kid is introduced, enables seamless, automated key rotation without system downtime.
4. Critical JWT Security Pitfalls
When implementing JWTs, developers must avoid several notorious security flaws:
1. The none Algorithm and Algorithm Confusion Vulnerabilities
In early JWT libraries, a major vulnerability existed where servers would accept tokens with the algorithm header "alg": "none". This told the verifier that the token was unsigned, and the server would accept the claims blindly without checking a signature.
Another dangerous historical exploit is the RS256-to-HS256 algorithm confusion attack. In this scenario, an attacker modifies a token’s header from asymmetric RS256 (which uses a public/private key pair) to symmetric HS256 (which uses a shared secret). The attacker then signs the token using the target service’s publicly available RS256 public key as the HMAC secret. If the backend verification server is incorrectly configured to accept any algorithm specified in the token header, it may attempt to verify the signature using the symmetric HS256 algorithm, treating the service’s own public key as the HMAC shared secret. Because the attacker signed the token with that exact public key, the verification succeeds.
- Mitigation: JWT verifiers must never rely on the
algparameter inside the token’s header to dynamically choose the verification algorithm. Instead, the verifying application must be explicitly configured to restrict and enforce a hardcoded whitelist of allowed algorithms (e.g., enforcing only['RS256']or only['HS256']for a given endpoint). While modern libraries provide built-in safeguards to help mitigate algorithm confusion, security remains the developer’s responsibility to ensure that the verification function is correctly configured to enforce these bounds rather than implicitly trusting incoming headers.
2. Weak Secret Keys
If you use HS256 with a short, guessable secret (like "my-secret-123"), attackers can run offline brute-force attacks on intercepted tokens to recover the secret. Once they have the secret, they can forge valid tokens with administrative privileges.
- Mitigation: Use strong, cryptographically random secret keys that provide at least 32 bytes (256 bits) of entropy. High-entropy secrets and random keys can be generated offline or using secure key-generation utilities.
3. Client-Side Storage Trade-offs
Deciding where to store JWTs on the client side requires balancing security trade-offs, as there is no single “best practice” suitable for all architectures:
- Local Storage / Session Storage: Offers simple implementation for Single Page Applications (SPAs). It reduces the risk of traditional ambient-credential CSRF because the browser does not automatically attach local storage tokens to outgoing HTTP requests. However, regardless of the client-side storage choice, authorization decisions must always be strictly validated and enforced server-side. Developers must also configure and verify the token’s audience (
aud) and application boundaries to prevent cross-domain token leaks. Furthermore, local storage remains highly susceptible to Cross-Site Scripting (XSS): if an attacker successfully injects a malicious script, they can easily read storage and exfiltrate the token. - Cookies (with
HttpOnlyandSecureflags): Protects against XSS exfiltration because client-side JavaScript cannot access the cookie contents. However, because the browser automatically attaches cookies to outgoing cross-site requests, this approach introduces vulnerability to Cross-Site Request Forgery (CSRF), which requires mitigation using appropriateSameSiteconfigurations, anti-CSRF tokens, or double-submit cookies. - In-Memory Storage: Storing the JWT solely within an active JavaScript variable (e.g., in React state) does not protect against XSS, as an active XSS exploit can still intercept or utilize tokens stored in memory. However, it reduces token persistence because the token disappears when the application’s page lifecycle ends or the tab is closed. To handle page refreshes, this architecture typically relies on a separate short-lived
HttpOnlyrefresh cookie or a token exchange endpoint to fetch a new access token seamlessly.
4. Failure to Validate Standard Claims
A valid cryptographic signature only proves that the JWT has not been altered since it was issued. It does not mean the token is currently valid.
- Mitigation: Applications should always validate standard claims such as
exp(Expiration Time) andnbf(Not Before) to ensure the token is active, as well asiss(Issuer) andaud(Audience) to verify that the token was intended for the specific application receiving it. When validatingexpandnbf, applications should also account for clock skew—small timing discrepancies between servers in distributed systems—by configuring a small tolerance window (typically 1 to 2 minutes) during claim evaluation.
5. Access Tokens vs. Refresh Tokens
For robust security, stateless architectures should distinguish between access tokens and refresh tokens:
- Access JWTs: These are short-lived tokens used to authorize individual API requests. While they are often configured to expire within minutes (such as 5 to 15 minutes), the optimal lifespan is not a universal standard and must be selected based on the application’s risk tolerance, performance requirements, and distributed architecture. A shorter lifespan minimizes the window of opportunity if a token is compromised, which is vital because stateless tokens cannot be easily revoked globally before they expire.
- Refresh Tokens: These require separate storage and rotation strategies. Because of their duration, they require strict separate storage configurations (such as an
HttpOnlycookie restricted to the authorization path), server-side tracking for revocation, and refresh token rotation (where using a refresh token invalidates the old one and issues a new pair).
Further Reading & Resources
Interactive Tools
- /jwt-debugger/ – Decode JWT headers, payloads, and claims locally in your browser.
- /rsa-generator/ – Generate high-entropy RSA public/private key pairs for token signing.
- /password-generator/ – Create high-entropy secret keys for HMAC-SHA256 tokens.
- /base64-encoder-decoder/ – Encode and decode strings in standard or Base64URL formats.
Related Guides
- Modern Web Cryptography – Master symmetric encryption, hashing, and digital signatures.
- Production-Grade API Practices – Implement rate limiting, versioning, and idempotency keys.
- Zero-Trust Network Perimeter Architecture – Design security architecture around identity and mTLS.
By understanding the internal mechanics and securing the validation pipeline of your JSON Web Tokens, you can build robust stateless authentication architectures that significantly reduce authorization risks. Combining rigorous backend signature verification and strict claim validation with secure token storage and local-first debugging tools during development will help minimize the attack surface of your users’ sessions.
