JWTAuthenticationSecurityAPIDeveloper Tools

JWT Tokens Explained: What They Are and How to Decode Them Safely

By Hamid Abderrahim
JWT Tokens Explained: What They Are and How to Decode Them Safely

JWT Tokens Explained: What They Are and How to Decode Them Safely

If you have built or consumed a modern web API, you have almost certainly encountered JSON Web Tokens — the long, dot-separated strings that look like eyJhbGciOiJSUzI1NiJ9.eyJ1c2VySWQiOjEyM30.signature. They appear in Authorization headers, in cookies, and in localStorage. They are fundamental to modern authentication systems, yet they are frequently misunderstood and misused in ways that create serious security vulnerabilities.

This guide explains exactly what JWTs are, how they encode information, how the signature verification works, the most common security mistakes, and how to decode a JWT token without sending it to any third-party service.

What Is a JWT?

A JSON Web Token is a compact, URL-safe token format defined in RFC 7519. It allows one party to securely transmit claims (statements about a user or entity) to another party as a JSON object.

The key word is "securely" — JWTs are digitally signed, meaning the recipient can verify the token has not been tampered with since it was issued. This makes them useful for authentication and authorization: after a user logs in, the server issues a JWT. The client stores it and sends it with every subsequent request. The server verifies the signature and trusts the claims inside.

The Three-Part Structure

A JWT consists of three Base64url-encoded parts separated by dots:

header.payload.signature

Part 1: Header

The header is a JSON object that describes the token type and signing algorithm:

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

Common algorithm values:

  • HS256 — HMAC with SHA-256 (symmetric: same key signs and verifies)
  • RS256 — RSA with SHA-256 (asymmetric: private key signs, public key verifies)
  • ES256 — ECDSA with P-256 (asymmetric, smaller keys than RSA)

Part 2: Payload (Claims)

The payload is a JSON object containing the claims — pieces of information about the user and the token:

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

Standard claims:

  • sub (Subject) — typically the user's ID
  • iss (Issuer) — who issued the token (e.g., auth.example.com)
  • aud (Audience) — intended recipient of the token
  • exp (Expiration Time) — Unix timestamp when the token expires
  • iat (Issued At) — when the token was issued
  • nbf (Not Before) — token is not valid before this timestamp
  • jti (JWT ID) — unique identifier for the token

Custom claims — any application-specific data (user role, permissions, tenant ID).

Part 3: Signature

The signature verifies the token's integrity. For HS256, it is computed as:

HMAC-SHA256(
  base64url(header) + "." + base64url(payload),
  secret_key
)

The server holds the secret key. When it receives a JWT, it recomputes the signature and compares it to the token's signature. If they match, the token has not been modified. If they differ, the token was tampered with.

For RS256, the server signs with its private RSA key and clients verify with the public key — enabling any party with the public key to verify tokens without needing the private signing key.

What Decoding a JWT Tells You

Decoding a JWT simply Base64url-decodes the header and payload sections. This reveals all the claims inside — the user ID, roles, permissions, expiration time, and any custom data the issuer included.

Important: Decoding does NOT verify the signature. Anyone can decode any JWT by Base64-decoding the first two sections. The payload is not encrypted — it is just encoded.

This means you should never put sensitive data (passwords, credit card numbers, private keys) inside a JWT payload. The payload is readable by anyone who has the token.

Use our JWT Decoder tool to decode any JWT directly in your browser. No token is sent to any server — decoding happens client-side using JavaScript's atob(). This is safe: you're just Base64-decoding public information.

JWT vs. Sessions: When to Use Each

JWTServer-Side Session
StorageClient (localStorage, cookie)Server (database, cache)
StatelessYesNo
RevocationDifficult (must wait for expiry)Easy (delete from DB)
ScalingEasy (no shared state)Requires session store
Token sizeLarger (encoded claims)Small (session ID only)
Best forMicroservices, APIs, mobile appsTraditional web apps

Critical Security Mistakes

1. The alg: none Vulnerability

The JWT spec originally allowed an algorithm value of none, meaning no signature is required. Vulnerable implementations would accept unsigned tokens as valid. An attacker could craft:

{"alg": "none", "typ": "JWT"}
{"sub": "1", "role": "admin"}

And the server would trust it. This allowed complete authentication bypass.

Fix: Always explicitly check the alg value in your JWT library configuration. Never allow none. Whitelist only the algorithms you use.

2. Confused Algorithm Attack (RS256 to HS256)

Some libraries verify a JWT using the algorithm specified in the header. An attacker who knows the server's public key (which may be publicly available) can change the algorithm to HS256 and sign the token with the public key. The server then verifies using HMAC with the public key — which matches.

Fix: Hard-code the expected algorithm on the server side. Do not trust the alg field in the token header.

3. Storing Tokens in localStorage

localStorage is accessible to JavaScript running on the page, making tokens stored there vulnerable to XSS attacks. If any third-party script or browser extension can inject JavaScript, it can steal all localStorage tokens.

Fix: For highest security, store JWTs in HttpOnly, Secure, SameSite=Strict cookies. These are inaccessible to JavaScript.

4. Not Validating exp

Not checking the expiration claim means tokens are valid forever, even after a user logs out or a token is compromised.

Fix: Always validate exp in your middleware. Most JWT libraries do this automatically when configured correctly.

5. Putting Sensitive Data in Payload

As noted above, the payload is just Base64-encoded — anyone with the token can read it.

Fix: Store only non-sensitive identifiers (user ID, role name) in the payload. Fetch sensitive data from the database when needed.

6. No Token Rotation or Revocation Strategy

JWTs are stateless, which means once issued, they cannot be revoked until they expire. If a user's token is stolen, the attacker has access until the token expires.

Fix: Use short expiry times (15–60 minutes) for access tokens combined with longer-lived refresh tokens stored server-side. Implement a token revocation list (deny list) for critical actions like password changes.

Decoding a JWT in Code

function decodeJWT(token) {
  const [headerB64, payloadB64] = token.split('.');
  const header = JSON.parse(atob(headerB64.replace(/-/g, '+').replace(/_/g, '/')));
  const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
  return { header, payload };
}

The replace calls convert Base64url encoding (- and _) back to standard Base64 (+ and /) before decoding with atob().

Summary

JWTs are a powerful tool for stateless authentication in modern web APIs, but they carry significant security responsibility.

Key takeaways:

  • A JWT has three parts: Header, Payload, Signature — all Base64url-encoded
  • The payload is readable by anyone with the token — never include sensitive data
  • The signature verifies integrity; decoding alone does not verify authenticity
  • Always validate exp, use strong algorithms (RS256, ES256), and reject none
  • Store tokens in HttpOnly cookies for XSS protection
  • Use short expiry times and refresh token rotation
  • Decode any JWT safely in your browser with the JWT Decoder tool