JWT Tokens Demystified: Structure, Security, and Common Mistakes
JSON Web Tokens (JWTs) have become the de facto standard for API authentication. They appear in OAuth 2.0 flows, single sign-on systems, and microservice architectures. Yet despite their ubiquity, many developers treat JWTs as opaque blobs — copy-paste them into headers and hope for the best.
This guide breaks down exactly how JWTs work, the security model they rely on, and the mistakes that lead to vulnerabilities. If you work with APIs, this is foundational knowledge.
The Three-Part Structure
A JWT is a string with three Base64URL-encoded sections separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|_____________HEADER_____________|._________________PAYLOAD__________________|.___________SIGNATURE___________|
Header
The header declares the token type and the signing algorithm. The two most common algorithms are:
- HS256 — HMAC with SHA-256. Symmetric: the same secret key signs and verifies.
- RS256 — RSA with SHA-256. Asymmetric: a private key signs, a public key verifies.
{
"alg": "HS256",
"typ": "JWT"
}
Payload (Claims)
The payload contains claims — statements about the user and metadata. There are three types of claims:
Registered claims are standardized by RFC 7519:
iss(Issuer) — Who created the tokensub(Subject) — Who the token is about (usually a user ID)aud(Audience) — Who the token is intended forexp(Expiration) — Unix timestamp when the token expiresiat(Issued At) — Unix timestamp when the token was creatednbf(Not Before) — Token is not valid before this timejti(JWT ID) — Unique identifier for the token
{
"sub": "user_123",
"name": "Jane Developer",
"role": "admin",
"iat": 1712000000,
"exp": 1712086400
}
Public claims are defined by the application (like name and role above). Private claims are custom claims agreed upon between parties.
Signature
The signature is computed over the header and payload:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
The signature ensures integrity (the payload has not been tampered with) and authenticity (the token was issued by someone who knows the secret). It does not provide encryption — the payload is only Base64-encoded, not encrypted. Anyone can read it.
How JWT Authentication Works
The typical flow looks like this:
- User sends credentials (username/password) to the authentication server.
- Server validates credentials and creates a JWT signed with a secret key.
- Server returns the JWT to the client.
- Client stores the JWT (usually in memory or an HTTP-only cookie).
- Client sends the JWT with each API request in the
Authorization: Bearer <token>header. - API server validates the signature and checks claims (expiration, audience, etc.).
The key advantage over session-based authentication is that the server does not need to store session state. The JWT is self-contained — all the information needed to validate it is in the token itself (plus the secret key). This makes JWTs particularly useful in distributed systems where you do not want to share session stores across services.
Base64URL vs Base64
JWTs use Base64URL encoding, not standard Base64. The differences are small but important:
+is replaced with-/is replaced with_- Padding (
=) is removed
// Decoding Base64URL in JavaScript
function base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
return atob(str);
}
This matters when you are building your own JWT decoder. Using standard atob() directly on a JWT segment will sometimes work (when there happen to be no - or _ characters) and sometimes fail — leading to intermittent bugs that are painful to debug.
Security Pitfalls and Common Mistakes
Mistake 1: Storing JWTs in localStorage
The localStorage API is accessible to any JavaScript running on your domain. If an attacker achieves XSS (Cross-Site Scripting), they can steal the token with a single line: localStorage.getItem('token').
Better approach: Store JWTs in HTTP-only cookies with the Secure and SameSite flags. This makes them inaccessible to JavaScript.
Mistake 2: Not Validating the Algorithm
The alg field in the header tells the server which algorithm to use for verification. A classic attack changes alg to "none", which some libraries accept without checking the signature at all.
Better approach: Always hardcode the expected algorithm on the server side. Never trust the alg header from the token.
Mistake 3: Using Weak Secrets
For HS256, the secret key must be at least 256 bits (32 bytes) of cryptographically random data. Secrets like "my-secret" or "password123" can be brute-forced in minutes.
# Generate a proper secret
openssl rand -base64 32
# Example output: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Mistake 4: No Expiration or Long Expiration
JWTs without an exp claim are valid forever. JWTs with a 30-day expiration are valid for 30 days even if the user's account is compromised. There is no built-in revocation mechanism.
Better approach: Short-lived access tokens (5-15 minutes) paired with longer-lived refresh tokens stored in HTTP-only cookies. This limits the damage window of a stolen token.
Mistake 5: Putting Sensitive Data in the Payload
Remember: the payload is only encoded, not encrypted. Do not put passwords, credit card numbers, or other secrets in JWTs. Anyone who intercepts the token can decode the payload trivially — you can do it yourself right now with our JWT Decoder.
Debugging JWTs in Practice
When you hit a 401 error with JWT authentication, here is a systematic debugging checklist:
- Decode the token — Check the payload for an
expclaim and compare it to the current Unix timestamp. This is the most common issue. - Check the audience — If the API validates the
audclaim, make sure the token was issued for the correct audience. - Verify the issuer — In multi-service architectures, the
issclaim must match what the API expects. - Inspect the header — Confirm the algorithm matches what the server expects.
- Check clock skew — If the token was issued by a different server, clock differences can cause
nbf(not before) validation to fail.
For teams working with machine learning APIs and model serving, JWT authentication is especially common. Platforms like those discussed on HeyTensor and open source utilities frequently use JWTs to secure model endpoints, making token debugging a daily skill for ML engineers.
JWTs vs Session Tokens
JWTs are not always the right choice. Here is a quick comparison:
Use JWTs when:
- You have a distributed system with multiple services
- You need stateless authentication
- You want to embed user claims in the token
- You are building an API consumed by third parties
Use sessions when:
- You need instant revocation (logout, account ban)
- You have a monolithic application
- You do not want token size growing with claims
- You need to track active sessions
Conclusion
JWTs are a powerful tool, but they require understanding. The three-part structure (header, payload, signature) is elegant in its simplicity. The security model is sound when implemented correctly. The common mistakes — localStorage storage, missing algorithm validation, weak secrets, long expirations, and sensitive data in payloads — are all avoidable with awareness.
Next time you encounter a JWT, decode it. Look at the header. Check the claims. Understand what you are working with instead of treating it as a magic string. Your debugging sessions will be shorter and your applications will be more secure.