Authentication2026-02-15

Understanding JWT Tokens: Structure, Security, and Best Practices

A deep dive into JSON Web Tokens — how they work, what the three parts mean, common vulnerabilities, and how to use JWTs securely in modern applications.

jwtauthenticationsecuritytokensoauth

Understanding JWT Tokens: Structure, Security, and Best Practices

JSON Web Tokens (JWTs) are everywhere in modern web development — they power authentication in REST APIs, OAuth 2.0 flows, and single sign-on systems used by millions of applications. Yet many developers use JWTs without fully understanding their structure, limitations, or security implications.

This guide covers everything you need to know about JWTs: how they're built, what each part means, and how to avoid common pitfalls that lead to security vulnerabilities.

What is a JWT?

A JWT (pronounced "jot") is a compact, URL-safe way to represent claims between two parties. The claims are encoded as a JSON object that is digitally signed — either using a secret (HMAC) or a public/private key pair (RSA or ECDSA).

JWTs are defined in RFC 7519 and consist of three Base64URL-encoded parts separated by periods:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The three sections are:

  1. Header — token type and signing algorithm
  2. Payload — the claims (data)
  3. Signature — cryptographic verification

The Header

The header is a JSON object encoded in Base64URL. It typically contains two fields:

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

The alg field specifies the signing algorithm. Common values:

  • HS256 — HMAC-SHA256 (symmetric, uses a shared secret)
  • RS256 — RSA-SHA256 (asymmetric, uses a private/public key pair)
  • ES256 — ECDSA with P-256 (asymmetric, smaller signatures than RSA)
  • noneNo signature (dangerous! Never accept this in production)

The Payload

The payload contains claims — statements about the user or additional metadata. Standard claims (defined in RFC 7519):

Claim Name Description
iss Issuer Who issued this token
sub Subject Who the token is about (usually user ID)
aud Audience Who should accept this token
exp Expiration When the token expires (Unix timestamp)
nbf Not Before Token not valid before this time
iat Issued At When the token was issued
jti JWT ID Unique identifier for the token

A typical payload looks like:

{
  "sub": "user_abc123",
  "name": "Jane Smith",
  "email": "jane@example.com",
  "roles": ["user", "admin"],
  "iat": 1709856000,
  "exp": 1709942400
}

Important: The payload is Base64URL-encoded, NOT encrypted. Anyone who has the token can read the payload. Never put sensitive data (passwords, credit card numbers, PII) in a JWT payload unless the token is also encrypted (JWE).

The Signature

The signature ensures the token hasn't been tampered with. For HMAC-SHA256 (HS256):

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

For RSA (RS256), the server signs with its private key and clients verify with the public key. This allows token verification without sharing secrets — useful for microservices and third-party integrations.

How JWT Authentication Works

  1. User logs in with credentials
  2. Server validates credentials, creates JWT with user claims
  3. Server signs JWT and returns it to the client
  4. Client stores JWT (localStorage, sessionStorage, or httpOnly cookie)
  5. Client sends JWT in the Authorization: Bearer <token> header
  6. Server verifies JWT signature and extracts claims
  7. Server authorizes the request based on the claims

The key advantage: the server doesn't need to store session state. The token carries everything needed for authorization.

Common JWT Vulnerabilities

1. The alg: none Attack

Some early JWT libraries accepted tokens with "alg": "none" — meaning no signature required. An attacker could modify any token, set the algorithm to none, and the server would accept it.

Fix: Always whitelist acceptable algorithms. Reject tokens with alg: none.

2. Algorithm Confusion (RS256 → HS256)

If a server uses RS256 (asymmetric), an attacker might send a token with "alg": "HS256", where the "secret" is the server's public key (which is, by definition, public). Libraries that don't explicitly verify the algorithm may accept this.

Fix: Always specify the expected algorithm when verifying. Never derive it from the token header.

3. Weak Secrets

HMAC-based JWTs using short or predictable secrets can be cracked offline. An attacker with a valid token can brute-force the secret using tools like hashcat.

Fix: Use cryptographically random secrets of at least 256 bits for HMAC. Better yet, use RS256 or ES256.

4. No Expiration

Tokens without an exp claim live forever. If a token is stolen, the attacker has permanent access.

Fix: Always set short expiration times (15–60 minutes for access tokens). Use refresh tokens for longer sessions.

5. Sensitive Data in Payload

Since JWT payloads are only Base64-encoded (not encrypted), storing sensitive data exposes it to anyone who captures the token.

Fix: Only include non-sensitive identifiers (user IDs) in the payload. Fetch sensitive data server-side.

6. JWT Storage in localStorage

Storing JWTs in localStorage exposes them to XSS attacks — any JavaScript on your page can read them.

Fix: Store tokens in httpOnly, Secure cookies. These aren't accessible to JavaScript, mitigating XSS token theft.

JWT Best Practices

For Token Generation

  • Use RS256 or ES256 for production (asymmetric — easier key rotation, no shared secrets)
  • Set short exp times: 15 minutes for access tokens
  • Always include iss, aud, sub, and exp claims
  • Use cryptographically random jti to enable token revocation

For Token Verification

  • Always verify the signature before trusting any claims
  • Validate all claims: exp, nbf, iss, aud
  • Whitelist algorithms — never trust the alg header value blindly
  • Reject tokens with alg: none
  • Check exp with a small clock skew tolerance (< 60 seconds)

For Token Storage and Transmission

  • Use httpOnly, Secure, SameSite=Strict cookies for web apps
  • Use Authorization: Bearer <token> header for API-to-API communication
  • Use HTTPS everywhere — JWTs in plaintext HTTP are trivially stolen
  • Never log full JWT tokens (they're credentials)

JWT vs Session Tokens

Feature JWT Session Token
Storage Client-side Server-side
Revocation Hard (requires denylist) Easy (delete session)
Scalability Stateless — scales easily Requires session store
Size Larger (~200-500 bytes) Small (~32 bytes)
Security Depends on implementation Easier to get right
Use case APIs, microservices, SSO Traditional web apps

Decoding JWTs

You can decode any JWT to inspect its contents using our free JWT Decoder tool. The decoder splits the token into its three parts, decodes the Base64URL encoding, and displays the header and payload as formatted JSON.

For testing and development, use the JWT Debugger to edit claims and re-encode tokens, or the JWT Encoder to create signed tokens with custom claims.

Refresh Token Pattern

For persistent authentication, combine short-lived access tokens with long-lived refresh tokens:

  1. Access Token: 15-60 minute expiry, stored in memory or httpOnly cookie
  2. Refresh Token: 7-30 day expiry, stored in httpOnly cookie only, stored server-side for revocation

When the access token expires, the client sends the refresh token to get a new access token — without requiring the user to log in again. The server validates the refresh token (checking it hasn't been revoked) before issuing a new access token.

This pattern gives you:

  • Short blast radius if an access token is stolen
  • Ability to revoke sessions (by invalidating the refresh token)
  • Persistent user sessions without security compromise

Conclusion

JWTs are a powerful tool for stateless authentication, but they're often misimplemented. The most critical rules:

  1. Always verify the signature with a whitelisted algorithm
  2. Always validate claims — especially exp, iss, and aud
  3. Use asymmetric algorithms (RS256/ES256) in production
  4. Store tokens securely — httpOnly cookies, not localStorage
  5. Keep tokens short-lived and use refresh tokens for persistence

Use our JWT Decoder to inspect any token, JWT Encoder to generate signed tokens, or PKCE Generator if you're implementing OAuth 2.0 authorization code flow with PKCE.