JWT Tokens Explained — How Authentication Works in Modern Web Apps

Learn how JSON Web Tokens work, what's inside them, how to decode and verify them, and common security pitfalls. Practical guide for developers.

Andreas · April 16, 2026 · 10 min read

Introduction

JSON Web Tokens (JWTs) are everywhere. If you've built a web app in the last decade, you've probably used them for authentication. They're in your Authorization headers, your cookies, and your API requests. But what's actually inside a JWT? How do they work? And what can go wrong?

This article breaks down the anatomy of a JWT, explains how signing and verification work, and covers the security pitfalls that catch developers off guard.

What Is a JWT?

A JWT is a compact, URL-safe string that represents a set of claims. It's used to transmit information between two parties in a way that can be verified and trusted. The most common use case is authentication: after a user logs in, the server issues a JWT that the client includes with subsequent requests to prove their identity.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Three parts separated by dots. That's it. No binary encoding, no special protocol — just a string you can pass in an HTTP header.

Anatomy of a JWT

Part 1: Header

The first segment is the header, Base64URL-encoded:

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

It specifies the signing algorithm (HS256 = HMAC-SHA256) and the token type. Other common algorithms include RS256 (RSA-SHA256), ES256 (ECDSA), and none (unsigned — dangerous, we'll get to that).

Part 2: Payload

The second segment contains the claims — the actual data:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "role": "admin"
}

Standard claims include:

  • sub (subject): who the token is about
  • iat (issued at): when the token was created (Unix timestamp)
  • exp (expiration): when the token expires
  • iss (issuer): who created the token
  • aud (audience): who the token is intended for

You can add any custom claims you want (role, email, permissions, etc.). Just remember that the payload is not encrypted — anyone can decode it. Use the JWT decoder tool to see this in action: paste any JWT and instantly see the decoded header and payload.

Part 3: Signature

The third segment is the signature, created by:

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

The signature ensures the token hasn't been tampered with. If someone changes a single character in the header or payload, the signature won't match and the server will reject the token.

How JWT Authentication Works

  1. User logs in with username and password
  2. Server verifies credentials against the database
  3. Server creates a JWT with the user's ID, role, and expiration time, signs it with a secret key
  4. Server sends the JWT to the client (in the response body or a cookie)
  5. Client stores the JWT (localStorage, sessionStorage, or cookie)
  6. Client includes the JWT in the Authorization header of subsequent requests: Authorization: Bearer <token>
  7. Server verifies the signature on each request and extracts the user info from the payload

The key advantage: the server doesn't need to store session state. The JWT itself contains all the information needed to authenticate the request. This makes JWTs popular in microservices architectures where multiple services need to verify authentication independently.

JWTs vs Session Cookies

Feature JWT Session Cookie
State storage Client-side Server-side
Scalability Stateless, easy to scale Requires shared session store
Revocation Hard (token is valid until expiry) Easy (delete the session)
Size Larger (carries all claims) Small (just a session ID)
Cross-domain Works easily with CORS Tricky with cross-domain

JWTs aren't universally better. For traditional server-rendered apps, session cookies are simpler and easier to revoke. JWTs shine in API-first architectures, single-page apps, and microservices.

Security Pitfalls

1. The alg: none Attack

Some JWT libraries accept tokens with "alg": "none", which means no signature verification. An attacker can forge a token, set the algorithm to none, and the server accepts it. Always validate the algorithm on the server side and reject none.

2. Storing Tokens in localStorage

localStorage is accessible to any JavaScript running on the page. If your site has an XSS vulnerability, an attacker can steal the JWT. Prefer HttpOnly cookies for token storage — they're not accessible via JavaScript.

3. Not Checking Expiration

Always check the exp claim. A JWT without an expiration is valid forever, even after the user changes their password or their account is suspended. Set short expiration times (15 minutes to 1 hour) and use refresh tokens for longer sessions.

4. Sensitive Data in the Payload

The payload is Base64-encoded, not encrypted. Anyone who intercepts the token can decode it instantly. Never put passwords, credit card numbers, or other sensitive data in the payload. Paste a JWT into the decoder to see how easily the contents are readable.

5. Weak Secrets

For HMAC-signed tokens, the secret key must be strong. A short or guessable secret can be brute-forced. Use at least 256 bits of randomness. For RS256, use a proper RSA key pair.

Decoding JWTs for Debugging

During development, you'll constantly need to inspect JWT contents. Common scenarios:

  • Checking claims: Is the user ID correct? Is the role right?
  • Debugging expiration: Has the token expired? When was it issued?
  • Verifying structure: Are all required claims present?

You can decode JWTs in several ways:

  • Use the JWT decoder tool for a visual breakdown with expiration checking
  • In the terminal: echo '<payload>' | base64 -d | python -m json.tool
  • In code: most JWT libraries have a decode-without-verify function for debugging

Token Refresh Flow

Since JWTs should have short lifetimes, you need a way to get new tokens without making the user log in again. The standard approach:

  1. Issue two tokens at login: a short-lived access token (15 min) and a long-lived refresh token (7 days)
  2. Client uses the access token for API requests
  3. When the access token expires, client sends the refresh token to a dedicated endpoint
  4. Server verifies the refresh token and issues a new access token
  5. If the refresh token is expired or revoked, the user must log in again

Store the refresh token in an HttpOnly cookie and the access token in memory (not localStorage). This limits the blast radius of XSS attacks.

Conclusion

JWTs are a powerful tool for stateless authentication, but they come with tradeoffs. Understand what's inside them, sign them properly, set short expirations, and don't store them where JavaScript can reach them.

When debugging, the JWT decoder gives you instant visibility into any token's contents and expiration status — entirely in your browser, so sensitive tokens never leave your machine.

Related Tools

Comments