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.