JWT Tokens Demystified: Structure, Security, and Common Mistakes

March 18, 2025 · 10 min read · By Michael Lip

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:

{
  "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:

{
  "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:

  1. User sends credentials (username/password) to the authentication server.
  2. Server validates credentials and creates a JWT signed with a secret key.
  3. Server returns the JWT to the client.
  4. Client stores the JWT (usually in memory or an HTTP-only cookie).
  5. Client sends the JWT with each API request in the Authorization: Bearer <token> header.
  6. 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:

// 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:

  1. Decode the token — Check the payload for an exp claim and compare it to the current Unix timestamp. This is the most common issue.
  2. Check the audience — If the API validates the aud claim, make sure the token was issued for the correct audience.
  3. Verify the issuer — In multi-service architectures, the iss claim must match what the API expects.
  4. Inspect the header — Confirm the algorithm matches what the server expects.
  5. 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:

Use sessions when:

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.