← Back to Blog

JWT Tokens Explained: Structure, Security, and Decoding

March 25, 2026 · 7 min read

JSON Web Tokens are everywhere. If you have built or consumed a modern web API, you have almost certainly run into one. That long dot-separated string in an Authorization: Bearer ... header? That is a JWT. Knowing how it works helps you debug auth issues, catch security misconfigurations, and make better decisions when designing token-based systems.

What Is a JWT?

A JSON Web Token (JWT, pronounced "jot") is a compact, URL-safe way to represent claims between two parties. RFC 7519 defines the spec. In practice, JWTs show up most often in authentication flows: user logs in, server issues a JWT, client sends it with every subsequent request to prove who it is.

The key difference from session-based auth is that a JWT is self-contained. The token carries everything the server needs to authenticate and authorize the request. No session store, no database lookup. That makes JWTs a natural fit for stateless architectures, microservices, and serverless functions.

The Three Parts of a JWT

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNjE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

|_________ Header _________|._________ Payload ___________|._______ Signature ________|

1. Header

The header is a JSON object describing how the token is signed:

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

alg specifies the signing algorithm (HS256, RS256, ES256, etc.) and typ is always "JWT". Base64url-encode this object and you get the first segment of the token.

2. Payload (Claims)

The payload contains claims, which are just statements about the user and metadata. The spec defines three categories. Registered claims are the standard optional ones: iss (issuer), sub (subject), aud (audience), exp (expiration), nbf (not before), iat (issued at), and jti (unique ID). Public claims come from the IANA JWT Claims Registry or use collision-resistant URIs. Private claims are whatever custom fields you and the consumer agree on, like role, org_id, or permissions.

Example payload:

{
  "sub": "user_8x4k2",
  "name": "Alice",
  "role": "admin",
  "iat": 1616239022,
  "exp": 1616242622
}

One thing I see people get tripped up on: the payload is encoded, not encrypted. Anyone with the token can decode it and read every claim. Do not put passwords, credit card numbers, or anything sensitive in there.

3. Signature

The signature is what prevents tampering. For HS256, it is computed like this:

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

When the server receives a JWT, it recomputes this signature using its secret key and compares the result to the signature in the token. Match? The token is legit and unmodified. Mismatch? Rejected.

Symmetric vs. Asymmetric Signing

HS256 (Symmetric)

Both the token issuer and verifier share the same secret key. Simple to set up, but it gets awkward when multiple services need to verify tokens. You end up distributing the secret everywhere, which increases the attack surface.

RS256 / ES256 (Asymmetric)

The issuer signs with a private key and verifiers only need the public key. This is the standard approach when an identity provider (Auth0, Keycloak, AWS Cognito) issues tokens and many downstream services verify them. The public key is typically published at a JWKS (JSON Web Key Set) endpoint like /.well-known/jwks.json.

Common JWT Security Pitfalls

The "alg: none" Attack

Some JWT libraries will happily accept tokens with "alg": "none", meaning no signature verification at all. An attacker can forge any claims they want. Reject alg: none in production and explicitly specify the expected algorithm when verifying.

Algorithm Confusion

Say your server expects RS256 but an attacker sends an HS256 token signed with the public key (which is, well, public). A poorly written library might verify it as valid. The fix is simple: hardcode the expected algorithm rather than trusting the token's header.

Missing Expiration

Tokens without an exp claim never expire. If one gets stolen, it grants permanent access. Set a short expiration (15 minutes for access tokens) and use refresh tokens for long-lived sessions.

Storing JWTs in localStorage

Any JavaScript running on the page can read localStorage, which makes tokens vulnerable to XSS attacks. For browser-based apps, storing JWTs in httpOnly cookies is usually a better call since JavaScript cannot access them.

How to Decode a JWT for Debugging

The header and payload are just Base64url-encoded JSON, so you can decode them in any language:

# Bash (using jq)
echo "eyJhbGciOiJIUzI1NiJ9" | base64 -d | jq .

// JavaScript
JSON.parse(atob("eyJhbGciOiJIUzI1NiJ9"))

That said, when I am in the middle of debugging something, I usually just paste the token into an online decoder. Faster than writing code, and you can check expiration times at a glance.

Paste a JWT and instantly see the decoded header, payload, and expiration status. No data is sent to any server.

Try the Free JWT Decoder

When Not to Use JWTs

JWTs are not always the right tool. Plain old server-side sessions with a session ID in a cookie are simpler, easier to revoke, and perfectly fine for monolithic apps. I would reach for JWTs when you actually need stateless auth across multiple services or when you are building a public API that third parties consume. Otherwise, keep it simple.

Frequently Asked Questions

Can I decode a JWT without the secret key?

Yes. The header and payload are only Base64url-encoded, not encrypted. Anyone can decode them and read the claims. The secret key is only needed to verify the signature, which proves the token was issued by a trusted party and has not been tampered with. Never store sensitive data in a JWT payload.

What is the difference between HS256 and RS256?

HS256 (HMAC-SHA256) is a symmetric algorithm: the same secret key is used to sign and verify the token. RS256 (RSA-SHA256) is asymmetric: a private key signs the token and a public key verifies it. RS256 is preferred in distributed systems where multiple services need to verify tokens without sharing a secret.

How do I handle expired JWT tokens?

Check the exp (expiration) claim in the payload. If the current Unix timestamp exceeds the exp value, the token is expired and should be rejected. To avoid disrupting users, implement a refresh token flow: issue short-lived access tokens (15 minutes) alongside a longer-lived refresh token that can request a new access token.