If you've built anything with a modern API, OAuth login, or a single-page app in the last several years, you've used JWTs — JSON Web Tokens. They show up in Authorization headers, cookies, localStorage, and URL parameters. But a surprising number of developers treat them as magic strings without understanding what's actually inside — which leads to some serious security mistakes.
Paste any JWT into the DevToolShack JWT Decoder and you'll see exactly what's inside instantly. This article explains what you're looking at and why it matters.
What Is a JWT?
A JSON Web Token is a compact, self-contained way to transmit information between parties as a JSON object. The key word is self-contained — the token carries its own claims about who the user is and what they're allowed to do, without requiring a database lookup on every request.
A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSBMb3ZlbGFjZSIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Three Base64url-encoded sections separated by dots. Each section has a specific purpose.
The Three Parts
Part 1: Header
The first section decodes to a JSON object describing the token type and signing algorithm:
{
"alg": "HS256",
"typ": "JWT"
}
alg tells the receiver which algorithm was used to sign the token. Common values: HS256 (HMAC-SHA256, symmetric), RS256 (RSA-SHA256, asymmetric), ES256 (ECDSA, asymmetric).
Part 2: Payload (Claims)
The second section is the actual data — a set of "claims" about the user or session:
{
"sub": "1234567890",
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
Standard claim names:
| Claim | Name | Meaning |
|---|---|---|
sub | Subject | Who the token is about (usually user ID) |
iss | Issuer | Who created the token |
aud | Audience | Who the token is intended for |
exp | Expiration | Unix timestamp when the token expires |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Token is invalid before this timestamp |
jti | JWT ID | Unique identifier for this token |
Part 3: Signature
The third section is a cryptographic signature over the header and payload. For HS256, it's computed as:
HMAC-SHA256(
base64url(header) + "." + base64url(payload),
secret_key
)
The signature lets the receiver verify that the token hasn't been tampered with. If anyone modifies the payload (say, changing "role": "user" to "role": "admin"), the signature becomes invalid.
The Critical Distinction: Signed, Not Encrypted
The signature proves the token is authentic and unmodified — it doesn't hide the contents. Think of it like a sealed envelope with a wax seal: the seal proves it hasn't been opened, but the contents are readable once you open it.
How Verification Works
When your server receives a JWT:
- Split the token on the dots — header, payload, signature
- Recompute the expected signature using the header + payload + your secret key
- Compare the computed signature to the received signature
- If they match, the token is valid and untampered
- Check that
exphasn't passed - Optionally check
issandaud
This is why JWTs are "stateless" — your server doesn't need to look up anything in a database. All the information needed to validate the token is in the token itself plus your secret key.
Common Security Mistakes
The "alg: none" Attack
Some early JWT libraries accepted "alg": "none" in the header, treating tokens with no signature as valid. Always verify that your library explicitly rejects alg: none.
Storing JWTs in localStorage
localStorage is accessible to any JavaScript on the page — including injected scripts from XSS attacks. For sensitive apps, store JWTs in HttpOnly cookies, which are inaccessible to JavaScript.
Not Validating Expiry
Always check the exp claim server-side. Don't rely on the client to discard expired tokens.
Weak Secrets with HS256
HS256 uses a shared secret. If that secret is short or guessable, attackers can brute-force valid signatures. Use a long random secret (256+ bits) or switch to RS256 with a proper keypair.
HS256 vs RS256: Which to Use?
| Feature | HS256 (Symmetric) | RS256 (Asymmetric) |
|---|---|---|
| Key type | Single shared secret | Public/private keypair |
| Who can sign | Anyone with the secret | Only the private key holder |
| Who can verify | Anyone with the secret | Anyone with the public key |
| Best for | Single service, simple setup | Microservices, third-party consumers |
| Secret exposure risk | Higher — secret shared with verifiers | Lower — public key is safe to share |
For microservice architectures where multiple services verify tokens but only one issues them, RS256 is the stronger choice — verifying services only need the public key, not the signing secret.