What's Actually Hiding Inside Your JWT?
If you've worked with any modern API authentication, you've touched a JWT — a JSON Web Token. You've pasted them into authorization headers, checked for their presence in requests, and probably stored them somewhere in local storage or a cookie. But have you ever actually read one?
Most developers treat JWTs as opaque strings — something the server produces and the client passes back. The token is valid or it isn't. The user is authenticated or they're not. What's inside feels irrelevant.
What's inside is deeply interesting, and understanding it changes how you think about security.
The Structure: Three Parts, One String
A JWT looks like three Base64URL-encoded strings joined by dots:
header.payload.signature. That structure isn't decorative — each part has a distinct
function.
The header identifies the type of token and the algorithm used to create the signature. A typical
header, decoded, looks like: {"alg": "HS256", "typ": "JWT"}. Simple. This tells any
consumer of the token how to verify the signature in the third part.
The payload is where the actual data lives. This is called the "claims" section, and it contains
information about the authenticated entity — typically the user. Standard claims include:
sub (subject — usually the user ID), iat (issued at — a Unix timestamp),
exp (expiration — another Unix timestamp), and aud (audience — which
systems the token is intended for). Applications also add custom claims: roles, permissions, email
addresses, organization IDs.
The payload of a JWT is not encrypted. It is encoded. Anyone with the token can decode and read the claims without knowing the secret key. The secret key is only needed to verify the signature — to know the token hasn't been tampered with.
The Signature: What Actually Makes It Secure
The third part of a JWT is the signature. It's generated by taking the encoded header, a dot, the encoded payload, a dot, and then applying a cryptographic function using a secret key that only the server knows.
This creates a mathematical binding between the signature and the content. If you change any character in the header or payload — even a single bit — the signature becomes invalid. The server can detect tampering by re-computing what the signature should be (it has the secret key) and comparing it to the one in the token.
What this means in practice: a user can decode their own JWT and read it. They can see their user ID, their roles, their expiration time. They cannot modify any of it and have that modified token accepted by the server — the signature would no longer match.
The Algorithm Confusion Attack
The header specifies the algorithm — and early JWT implementations had a critical flaw here. The "none" algorithm was a valid option: no signature required. Attackers could take a valid JWT, strip the signature, change the header to indicate no algorithm was needed, modify the payload to escalate their permissions, and some servers would accept it.
Modern JWT libraries reject the none algorithm by default. But the lesson is important: the algorithm specified in the header should be validated server-side, not trusted from the token itself. A server that signs tokens with RSA shouldn't also accept HMAC-signed tokens using the public key as the secret — another real attack vector from the early days of JWT adoption.
What Developers Get Wrong About JWTs
The most common mistake is using JWTs without expiration. A token without an exp claim
is valid indefinitely, which means a stolen token is valid indefinitely. Setting short expiration
times — 15 minutes to an hour — combined with refresh tokens is the standard approach for managing
this.
The second common mistake is putting sensitive data in the payload. Since the payload is readable by anyone with the token, you shouldn't store passwords, financial details, or other private information there. User IDs and roles are appropriate. Full social security numbers are not.
Next time a JWT causes an unexpected 401, DevToolkit's JWT decoder shows you the decoded header and payload instantly — plus the expiration status so you know exactly what went wrong.