Security

Understanding JWT Security: Structure, Decoding, and Pitfalls

JWTs are everywhere in modern auth — but most developers never learn how they actually work. Here is the structure, the security model, and the pitfalls.

ToolKit Pro TeamJune 2, 202610 min read
#jwt#security#authentication#api

JSON Web Tokens (JWTs) are the dominant authentication mechanism for modern APIs — they sit in every Authorization header, every cookie-based session, every OAuth callback. Yet most developers treat JWTs as opaque strings without understanding what is inside them or how the security model works. This guide fixes that. By the end, you will know the structure, the security properties, and the pitfalls that cause real-world breaches.

The three-part structure

A JWT is three base64url-encoded JSON objects separated by dots:

xxxxx.yyyyy.zzzzz
  │     │     │
  │     │     └── signature
  │     └──────── payload (claims)
  └────────────── header (algorithm + type)

A real example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTcxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

The header

The first segment decodes to a small JSON object describing the token’s algorithm and type:

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

Common algorithms:

  • HS256 — HMAC with SHA-256. Symmetric; the same secret signs and verifies.
  • RS256 — RSA signature with SHA-256. Asymmetric; private key signs, public key verifies.
  • ES256 — ECDSA with P-256 and SHA-256. Asymmetric, smaller than RSA.
  • none — explicitly no signature. Should never be used in production.

The payload (claims)

The second segment contains the claims — assertions about the user or session. Standard registered claims include:

  • iss — issuer: who created the token.
  • sub — subject: who the token is about (usually a user ID).
  • aud — audience: who the token is intended for.
  • exp — expiration time (Unix timestamp). Reject tokens past this.
  • nbf — not-before time. Reject tokens before this.
  • iat — issued-at time. When the token was created.
  • jti — unique JWT ID. Useful for revocation lists.

You can add any custom claims you want. Common custom claims: roles, scopes, email, tenant_id.

Critical: the payload is base64url-encoded, NOT encrypted. Anyone who has the token can decode and read the payload. Never put secrets, passwords, or sensitive PII in a JWT payload.

The signature

The third segment is the cryptographic signature. For HS256:

signature = HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret
)

The signature proves the token has not been tampered with. If anyone changes a single byte in the header or payload, the signature will not match. Verification recomputes the signature and compares — if it matches, the token is authentic.

base64url vs base64

JWTs use base64url encoding, not standard base64. The differences:

  • base64url uses - and _ instead of + and /.
  • base64url omits padding (= characters).
  • This makes the token URL-safe — it can go in query strings, headers, and cookies without escaping.

To decode a JWT segment in JavaScript, convert base64url to standard base64 first, then atob:

function decodeJwtSegment(seg) {
  // base64url -> base64
  let b64 = seg.replace(/-/g, "+").replace(/_/g, "/")
  // pad to length multiple of 4
  while (b64.length % 4) b64 += "="
  return JSON.parse(atob(b64))
}

Or just use our JWT Decoder — paste the token, get the decoded header and payload with no setup.

Decoding is not verifying

The single most important JWT security concept: decoding and verifying are different operations.

  • Decoding — reading the payload. Anyone can do this. No secret required. This is what a JWT Decoder does.
  • Verifying — checking the signature with the secret or public key. Only the server with the secret can do this for HS256; anyone with the public key can do it for RS256.

Never trust claims from a decoded-but-unverified token. An attacker can craft a token with any payload they want — admin: true, sub: someone_else. Only the signature proves authenticity.

Common JWT attacks (and how to prevent them)

1. The "alg: none" attack

Some vulnerable JWT libraries allowed clients to set alg to "none" in the header, then skipped signature verification. Attackers would craft a token with alg: "none", any payload, and an empty signature — and the server would accept it.

Defense: hard-code the allowed algorithms on the server. Never trust the alg in the token header. Reject any token that is not signed with your expected algorithm.

2. Algorithm confusion (RS256 → HS256)

If the server uses RS256 (asymmetric) and exposes its public key, an attacker can re-sign a token using HS256 with the public key as the HMAC secret. Vulnerable libraries then verify the token using HS256 with the public key — which matches the attacker’s signature.

Defense: enforce the expected algorithm. Do not let the token header dictate which verification path runs.

3. Storing tokens insecurely

JWTs in localStorage are readable by any JavaScript on the page — including XSS payloads. JWTs in cookies without HttpOnly and Secure flags can be stolen via XSS or intercepted over HTTP.

Defense: store access tokens in memory (and refresh tokens in HttpOnly, Secure, SameSite cookies). Implement a strict Content Security Policy to mitigate XSS.

4. Over-long token lifetimes

A JWT cannot be revoked before its exp — there is no central list (unless you implement one). If a token is stolen, the attacker has access until it expires.

Defense: keep access token lifetimes short (5–15 minutes). Use refresh tokens for longer sessions. Implement a revocation list (jti-based) for high-security applications.

5. Sensitive data in the payload

Because the payload is base64 (not encrypted), any data in it is readable by anyone with the token. Putting passwords, API keys, or PII in a JWT payload is a breach waiting to happen.

Defense: only put non-sensitive identity data in the payload. If you need to attach sensitive data, store it server-side keyed by the sub claim and fetch it after verification.

Practical checklist for production JWT usage

  • Use a well-maintained library (jose, jsonwebtoken, PyJWT) — do not roll your own.
  • Hard-code allowed algorithms server-side. Reject "none".
  • Set short exp (5–15 min) on access tokens.
  • Use HttpOnly, Secure, SameSite=Lax cookies for storage (not localStorage).
  • Validate iss, aud, and exp on every request.
  • Rotate signing keys periodically. Support a key ID (kid) header for smooth rotation.
  • Never put secrets or PII in the payload.
  • Implement CSP to mitigate XSS token theft.

Decoding JWTs for debugging

For debugging — inspecting claims, checking exp values, verifying iss/aud — you do not need verification. You just need to decode. Our JWT Decoder splits the token into header/payload/signature, decodes the base64url, and shows the JSON. For converting base64-encoded values generally (not just JWTs), use the Base64 Encode/Decode tool.

Reminder: paste only tokens you control or that are already expired. Treat production JWTs like credentials — they grant access until they expire.

The takeaway

JWTs are powerful but require care. The structure (header.payload.signature) is simple. The security model (signature = tamper protection, not encryption) is often misunderstood. The attacks (alg: none, algorithm confusion, XSS theft) are real and ongoing. Decode tokens for debugging with our JWT Decoder, encode/decode base64 with the Base64 tool, and always verify server-side with a vetted library.

Try the tools from this article

Free · No signup · Runs entirely in your browser

Private — runs in your browserInstant resultsFree forever

Related articles