Skip to content

JWT Authentication Explained: Structure, Claims, Algorithms, and Security Pitfalls

Explainer10 min readMarch 31, 2026
Table of Contents

JSON Web Tokens (JWTs) are the dominant authentication mechanism in modern web APIs. They are transmitted in Authorization headers, stored in cookies and local storage, decoded in middleware, and validated in every protected endpoint. Despite being ubiquitous, JWTs are consistently misunderstood and frequently misimplemented — in ways that create serious security vulnerabilities.

This guide explains exactly what a JWT is: its three-part structure, how the signature works, which algorithm to use and why, which claims matter and how to validate them, and the implementation mistakes (the "alg: none" attack, insecure storage, missing expiration validation) that have compromised real applications.

Understanding JWTs at this level takes about 15 minutes. The security knowledge will save you from the most common authentication vulnerabilities in the wild.

JWT Structure: Three Parts

A JWT is a string of three Base64url-encoded sections separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNzQzMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1 — Header: Describes the token itself. Typically:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg identifies the signing algorithm. typ is always "JWT". Some tokens add kid (Key ID) to indicate which key was used — needed when rotating signing keys.

Part 2 — Payload: Contains the claims — assertions about the authenticated entity. Typically:

{
  "sub": "1234567890",
  "name": "Alice",
  "email": "alice@example.com",
  "role": "admin",
  "iat": 1743000000,
  "exp": 1743003600
}

The payload is Base64url-encoded, not encrypted. Anyone with the token can decode and read it. Never put passwords, secret keys, credit card numbers, or other sensitive data in a JWT payload.

Part 3 — Signature: Created by the server using the algorithm specified in the header:

  • HMAC algorithms (HS256, HS384, HS512): HMAC-SHA256(base64url(header) + "." + base64url(payload), secret_key)
  • RSA/ECDSA algorithms: RSA-SHA256(base64url(header) + "." + base64url(payload), private_key)

The signature proves that the header and payload have not been tampered with since the server signed the token. If any byte in the header or payload changes, signature verification fails.

Decode a JWT tokenInspect header, payload, and expiration

Standard Claims

Claims are the key-value pairs in the JWT payload. Some are standardised by RFC 7519; the rest are application-specific.

Standard (registered) claims:

ClaimNameMeaning
issIssuerWho issued the token (your auth server URL)
subSubjectWho the token represents (user ID)
audAudienceWho should accept the token (your API identifier)
expExpiration TimeUnix timestamp after which the token is invalid
nbfNot BeforeUnix timestamp before which the token is invalid
iatIssued AtUnix timestamp when the token was created
jtiJWT IDUnique token identifier (enables revocation)

Critical validation rules:

1. Always validate `exp`. A token whose exp is in the past is expired and must be rejected regardless of valid signature. Accepting expired tokens is one of the most common JWT security mistakes.

2. Validate `iss`. Confirm the token was issued by your authentication server, not some other service.

3. Validate `aud`. If your token includes an audience claim, verify it matches your service. This prevents tokens issued for one service from being used on another.

4. Validate `nbf` if present. A token presented before its nbf time should be rejected.

Custom claims: Any key not in the registered list is an application-specific claim. Prefix custom claims with a namespace to avoid collisions: "https://api.yourapp.com/roles": ["admin"]. Short undocumented keys like "role" or "isAdmin" are fine for internal use; namespaced claims are recommended when tokens cross service boundaries.

Claim size: The JWT payload is included in every request as a header or cookie. Keep it small. Store only what the server needs for authorization decisions — typically user ID, roles, and expiration. User preferences, profile data, and large permission lists belong in the database, not the token.

Signing Algorithms: HS256 vs RS256 vs ES256

The alg header field determines how the signature is created and verified. Choosing the right algorithm for your architecture is critical.

**HS256, HS384, HS512 (HMAC-SHA-*):** Symmetric algorithms. The same secret key is used to sign and verify. All parties (the signer and all verifiers) must share the secret.

  • Appropriate when: You control all services that verify tokens. A monolith, or a small set of services where the secret can be securely distributed.
  • Avoid when: Third-party services need to verify your tokens, or tokens cross trust boundaries. You cannot distribute your signing secret to external services.

**RS256, RS384, RS512 (RSA-SHA-*):** Asymmetric algorithms. The private key signs; the public key verifies. The public key can be distributed freely — it cannot forge signatures.

  • Appropriate when: Multiple services verify tokens, third parties need to verify your tokens, or you publish a JWKS (JSON Web Key Set) endpoint for automatic key discovery.
  • Drawback: Larger signature size (256 bytes vs 32 bytes for HS256). RSA key generation and verification are slower than HMAC (though negligible for most web apps).

**ES256, ES384, ES512 (ECDSA-SHA-*):** Asymmetric like RSA, but using elliptic curve cryptography. Smaller keys and signatures than RSA at equivalent security levels.

  • Appropriate when: You want asymmetric signing with smaller payload overhead than RSA.
  • ECDSA implementation caution: ECDSA requires a secure random number for each signature. A flawed RNG can leak the private key. Use a well-reviewed library, never implement ECDSA manually.

PS256 (RSASSA-PSS): RSA with probabilistic signing. More secure than RS256 (deterministic RSA is vulnerable to certain side-channel attacks). Required by some regulatory standards. Supported by most modern JWT libraries.

Recommendation for new projects: RS256 for services that need to distribute verification capability; HS256 for purely internal auth servers where you control all verifiers.

The Security Vulnerabilities You Must Know

JWTs have a documented history of critical vulnerabilities in implementations. Every developer working with JWTs should know these attacks.

1. The "alg: none" attack. The JWT specification allows "alg": "none", meaning an unsigned token that any parser should accept without verification. Early JWT libraries that respected this field would accept a token with "alg": "none" and no signature as valid — allowing an attacker to forge any token. Fix: explicitly reject tokens with alg: none. Always specify the allowed algorithms when validating; never use the algorithm from the token header to choose the verification algorithm.

2. RS256 to HS256 confusion attack. When a server accepts both RS256 and HS256, an attacker can take the server's public RSA key (which is public!), craft a token signed with HS256 using that public key as the HMAC secret, and the server verifies it with the public key — which is the same value used as the HMAC secret. Fix: never accept both algorithm families on the same verification path. Specify the expected algorithm and reject anything else.

3. Missing expiration validation. A valid signature does not mean a valid session. Tokens remain cryptographically valid after exp. Servers that verify the signature but not the expiration accept tokens from logged-out or disabled users indefinitely. Fix: always check exp before accepting a token.

4. Insecure storage (XSS via localStorage). Tokens stored in localStorage are accessible to any JavaScript running on the page. An XSS vulnerability can exfiltrate the token, giving an attacker a persistent authenticated session. Fix: use HttpOnly cookies for token storage — JavaScript cannot read HttpOnly cookies. This shifts the attack surface from XSS token theft to CSRF, which is more controllable.

5. No revocation. JWTs are stateless — the server does not track issued tokens. Logging out, banning a user, or changing a password does not invalidate existing tokens until they expire. Fix: use short expiration (15 minutes for access tokens). Implement a revocation list (a database of jti values) for security-critical operations like password changes. Use refresh tokens with longer expiration for persistent sessions.

Access Tokens and Refresh Tokens

The standard pattern for JWT-based authentication uses two types of tokens with different lifetimes and storage.

Access token: A short-lived JWT (typically 15 minutes to 1 hour) included in every API request in the Authorization: Bearer <token> header. Short expiration limits the damage if a token is compromised — it becomes worthless after 15 minutes without server-side revocation. The server validates the token on every request but does not store it — the authentication is stateless.

Refresh token: A long-lived token (1 day to 90 days depending on security requirements) stored securely (HttpOnly cookie or secure device storage). Used only to obtain a new access token when the current one expires. The refresh token is validated by the auth server and stored in a database for revocation capability. On compromise, revoke the refresh token — the attacker cannot obtain new access tokens.

The token refresh flow:

1. Client sends request with access token → server validates, returns response

2. Access token expires → next request returns 401

3. Client sends refresh token to /auth/refresh endpoint

4. Auth server validates refresh token, issues new access token (and optionally rotates the refresh token)

5. Client retries original request with new access token

Refresh token rotation: On each refresh, issue a new refresh token and invalidate the old one. If an attacker has stolen the refresh token and uses it, the legitimate client's next refresh attempt fails (old token is invalid), alerting you to a compromise. Implement refresh token families — if a refresh token from a rotated-out family is used, invalidate the entire family.

Silent refresh: Implement a timer in the frontend that triggers a refresh request shortly before the access token expires (e.g., 1 minute before expiration). The user never sees the expiration — their session renews transparently.

Frequently Asked Questions

Is a JWT payload encrypted?
No. The payload (and header) are Base64url-encoded — which is reversible encoding, not encryption. Anyone with the token can decode and read the payload without a key. JWTs with encrypted payloads are called JWEs (JSON Web Encryption) and use a different structure. Standard JWTs (JWS — JSON Web Signature) only sign the payload for integrity; they do not hide the contents. Never put sensitive data like passwords or secrets in a standard JWT payload.
How long should a JWT access token last?
15 minutes is a common default for security-conscious implementations; 1 hour is common for lower-risk applications. The shorter the lifetime, the smaller the window for a compromised token to be abused without server-side revocation. Pair short access tokens with refresh tokens for sessions that persist across browser closes. Never use expiration longer than 24 hours for access tokens without a revocation mechanism.
Should I store JWTs in localStorage or cookies?
HttpOnly cookies are more secure than localStorage. JavaScript cannot read HttpOnly cookies, so XSS attacks cannot steal the token. localStorage is readable by any JavaScript on the page. The trade-off: HttpOnly cookies are vulnerable to CSRF (cross-site request forgery) attacks, which must be mitigated with SameSite=Strict/Lax and CSRF tokens. For most applications, HttpOnly cookies with SameSite protection is the recommended approach.
What does 'invalid signature' mean when decoding a JWT?
The token's contents (header or payload) have been modified after signing, the wrong secret key is being used for verification, the algorithm in the header does not match the algorithm the verifier is using, or the token was signed by a different service than the one verifying it. Decode the header to confirm the algorithm, check that the key used for verification matches the key used for signing, and verify the token has not been copied from a different environment (staging vs production often use different secrets).
Can I use the same JWT for authentication and authorization?
Yes, and this is common — the JWT contains both identity claims (sub, email) and authorization claims (roles, permissions). The practical limit is token size: large role/permission lists bloat every request header. If a user's permissions change frequently, short token expiration reduces the window where the token has stale permissions. For complex permission systems, keep only role identifiers in the token and resolve full permissions from the database in the authorization layer.

Summary

JWTs are an elegant mechanism when implemented correctly: stateless, verifiable, and self-contained. The failures happen at the implementation layer — accepting unsigned tokens, skipping expiration checks, choosing the wrong algorithm for multi-service architectures, storing tokens where XSS can reach them. Understand the structure, validate all standard claims, use short expiration with refresh token rotation, and store tokens in HttpOnly cookies. Done right, JWT authentication is both secure and operationally simple.

Try these tools

Related guides

All Guides