If you've built any kind of login system in the last decade, you've used JWTs — even if you didn't fully understand what was inside them. They show up in Authorization headers, in mobile app auth flows, in microservice-to-microservice calls. And there's one misconception about them that I see experienced developers get wrong often enough that it's worth addressing head-on before anything else.
The misconception that matters most
A JWT is not encrypted. It's encoded and signed. That distinction matters enormously, and if you only remember one thing from this article, make it this one: anyone who gets hold of a JWT — a browser extension, a proxy, someone reading your logs — can decode it and read everything inside, instantly, without any key. Encoding is not encryption. Base64url is just a different way of writing the same data, the same way hex or binary is. It's reversible by design, not by accident.
This is why you should never put a password, a credit card number, or any secret directly in a JWT payload. If you put it there, it's effectively public. What a JWT's signature protects against is tampering — it proves the token hasn't been modified since the server issued it. It does nothing to hide the contents.
The three parts — header.payload.signature
A JWT is always three Base64url-encoded segments separated by dots, and it always looks like this shape:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRla1B1bHNlIFVzZXIiLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6OTk5OTk5OTk5OX0.signature_here
Each section has a specific job:
Header — a small JSON object describing how the token is signed. Decode the first segment above and you get {"alg":"HS256","typ":"JWT"}. This tells the verifier which algorithm to use when checking the signature — HMAC-SHA256 in this case.
Payload — the actual data, called "claims." Decode the second segment and you get {"sub":"1234567890","name":"TekPulse User","iat":1700000000,"exp":9999999999}. This is plain JSON, just Base64url-encoded, nothing more.
Signature — this is the part that actually provides security. It's computed by taking the encoded header and payload, joining them with a dot, and running that string through a signing algorithm (HMAC or a public-key algorithm) using a secret or private key only the server knows.
Decoding one by hand
Try it yourself — take just the header segment from the example above, eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9, and Base64url-decode it. You'll get the header JSON directly. No key, no server call, no special tool needed — it's literally just text encoded differently. That's the point worth internalizing: decoding a JWT proves nothing about its authenticity. Anyone can do it.
What you can't do without the signing key is produce a valid signature for a payload you've modified. If you change "sub":"1234567890" to someone else's user ID and re-encode it, the signature from the original token will no longer match — a server verifying it will reject the token.
How verification actually works
When a server receives a JWT, it doesn't trust the payload until it has independently recomputed the signature and compared it. There are two common signing approaches:
HMAC (HS256, HS384, HS512) — symmetric. The same secret key is used to sign and to verify. This means every service that needs to verify tokens must have access to that same secret, which is fine for a single backend but gets awkward across many independent services.
RSA or ECDSA (RS256, ES256, etc.) — asymmetric. The server signs with a private key, and anyone with the corresponding public key can verify the signature — but cannot create new valid tokens. This is the better fit for microservices: you distribute the public key freely to every service that needs to verify tokens, while only the auth service holds the private key that issues them.
Common claims you'll see in almost every JWT
The JWT spec defines a handful of standard (but optional) claims: iss (issuer — who created the token), sub (subject — usually the user ID), exp (expiration — a Unix timestamp after which the token is no longer valid), iat (issued at), and aud (audience — who the token is intended for). None of these are enforced automatically by the format itself; it's the verifying application's job to check exp hasn't passed and that aud/iss match what's expected. A library that just decodes a JWT without checking these is not doing authentication — it's doing a JSON parse.
A mistake that actually happened in the wild
In 2015, researchers demonstrated an "alg confusion" attack against JWT libraries that trusted the alg field in the header without restricting it. If a server expected RS256 but a library let an attacker submit a token with "alg":"none" or swapped to HS256 using the public key as the HMAC secret, some implementations would accept forged tokens. The fix: always whitelist the expected algorithm on the verifying side rather than trusting whatever the token's header claims.
Where our tool fits in
Use our JWT Decoder to paste a token and instantly see the decoded header and payload — it's useful for debugging an auth flow, checking what claims your backend is actually issuing, or reading an expiry timestamp without writing code. One honest limitation worth stating clearly: it decodes the header and payload only — it does not validate the signature, because that would require the secret or public key, and we're not asking you to paste that into a browser tool. If you need to confirm a token is genuinely valid (not just well-formed), do that verification server-side with the actual signing key.