JWT Tokens Decoded: Structure, Security, and Best Practices
SecurityJWTAuthentication

JWT Tokens Decoded: Structure, Security, and Best Practices

What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe means of representing claims between two parties. It is the backbone of modern stateless authentication — the reason your API server does not need to hit a database on every request to verify who you are.

A JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkZyZWR5IiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

It consists of three Base64URL-encoded parts separated by dots: header.payload.signature.

The Header

The header is a JSON object that describes the token itself — specifically, what algorithm was used to sign it:

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

alg specifies the signing algorithm. The most common values are:

  • HS256 — HMAC-SHA256 (symmetric: same secret signs and verifies)
  • RS256 — RSA-SHA256 (asymmetric: private key signs, public key verifies)
  • ES256 — ECDSA-SHA256 (asymmetric, smaller signatures than RSA)

For microservices, prefer RS256 or ES256 so that individual services can verify tokens using a public key without ever having access to the private key that creates them.

The Payload

The payload contains the claims — statements about the user and session. The JWT specification defines "registered" standard claims, though none are required:

  • sub — subject (usually the user ID)
  • iss — issuer (which service created this token)
  • aud — audience (which service(s) should accept this token)
  • exp — expiration time (Unix timestamp; tokens after this time are invalid)
  • iat — issued at (Unix timestamp of creation)
  • jti — JWT ID (unique identifier, useful for implementing token blocklists)

You can add any custom claims: "role": "admin", "plan": "pro", "org": "acme-corp". Custom claims that are application-specific should be namespaced with a URL to avoid collisions: "https://myapp.com/role": "admin".

Remember: the payload is Base64URL encoded, not encrypted. Anyone with the token can read it. Keep it small and never include secrets.

The Signature

The signature is what makes JWTs trustworthy. For HS256, it is computed as:

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

The server verifies the signature on every request. If either the header or payload has been tampered with — even by a single character — the signature will not match and the token is rejected. This means the server can trust the contents of a valid JWT without a database lookup, making JWTs ideal for stateless, horizontally-scaled systems.

The Token Lifecycle

  1. Login: User provides credentials. Server validates them and issues a signed JWT.
  2. Storage: Client stores the JWT (in memory, a cookie, or localStorage — more on this below).
  3. Request: Client sends the JWT in the Authorization: Bearer <token> header on subsequent requests.
  4. Verification: Server validates the signature and checks exp. If valid, the request proceeds.
  5. Expiration: Once expired, the client must re-authenticate or use a refresh token.

Common Security Mistakes

1. Storing JWTs in localStorage

localStorage is accessible by any JavaScript running on the page — including injected scripts from XSS attacks. A compromised JWT means complete account takeover until the token expires. Prefer httpOnly; Secure; SameSite=Strict cookies, which are inaccessible to JavaScript entirely.

2. Accepting alg: none

The JWT spec allows an algorithm value of none, meaning the token is unsigned. Some early JWT libraries would accept these tokens if the header said none, even when verification was expected. Always explicitly specify which algorithms your library should accept during verification:

jwt.verify(token, secret, { algorithms: ['HS256'] });

3. Long Expiration Times

A JWT cannot be invalidated before its expiration time without maintaining a blocklist — which defeats the stateless advantage. Keep access token lifetimes short (5–15 minutes) and use short-lived refresh tokens (1–7 days) for obtaining new access tokens. If a user logs out or a token is compromised, the damage window is limited.

4. Putting Sensitive Data in the Payload

Passwords, API keys, payment details, and PII should never appear in a JWT payload. The payload is readable by anyone who has the token. Store only the minimum claims needed to authorize requests.

5. Not Validating aud and iss

Always validate the iss (issuer) and aud (audience) claims. A token issued by your authentication service for your mobile app should not be accepted by your admin API. Failing to validate these can allow tokens from one context to be used maliciously in another.

Refresh Token Strategy

The most robust pattern for production apps:

  • Issue short-lived access tokens (15 minutes) in the response body or a memory-only cookie
  • Issue a refresh token (7 days) in an httpOnly; Secure; SameSite=Strict cookie
  • When the access token expires, the client silently calls /auth/refresh to get a new one using the refresh token cookie
  • On logout, delete both tokens on the client and add the refresh token's jti to a short-lived server-side blocklist

Decoding JWTs

Use PureFormatter's JWT Decoder to instantly decode and inspect any JWT — header, payload, expiration time, and raw claims — all processed in your browser. No token is ever sent to any server, making it safe to use with real tokens during development and debugging.

Fredy
Written by
Fredy
Senior Developer & Technical Writer

Fredy is a full-stack developer with 8+ years of experience building web applications. He writes about developer tools, best practices, and the craft of clean code.