JWT Anatomy: Header, Payload, and Signature Explained
A deep dive into JSON Web Token structure, signing algorithms, common vulnerabilities, and a safe workflow for decoding tokens in development without exposing production secrets.
Decoding a JWT is not verifying it. Learn how signature verification works, the alg:none and algorithm-confusion attacks to avoid, which claims to check, and how to inspect tokens safely in development.
By codefunc
A decoded JWT looks trustworthy — clean JSON with a user id, roles, and an expiry. But decoding proves nothing. A forged token decodes to valid JSON just as easily as a real one. Only signature verification proves a token was issued by a party holding the signing key and has not been tampered with. Getting this step wrong is one of the most common — and most severe — authentication bugs.
Bottom line: Always verify the signature server-side before trusting any claim. Pin the expected algorithm, use the correct key type, and check exp, nbf, iss, and aud. Decoding is for reading; verification is for trusting.
A JWT is three Base64url segments: header.payload.signature. Decoding the first two is trivial and requires no key:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.<signature>
└─ header ─┘ └─ payload ─┘ └─ proof ─┘
The signature is computed over header.payload using a secret (HMAC) or a private key (RSA/ECDSA). Verification recomputes or checks that signature. If it does not match, the token is rejected — full stop.
Decoding answers "what does this claim?". Verification answers "can I trust it?".
There are two families:
| Family | Algorithms | Sign with | Verify with |
|---|---|---|---|
| Symmetric (HMAC) | HS256, HS384, HS512 | shared secret | the same shared secret |
| Asymmetric | RS256, ES256, PS256 | private key | public key |
With HMAC, the same secret both signs and verifies — so every service that verifies can also forge. With asymmetric algorithms, only the holder of the private key can sign; anyone can verify with the public key. For third parties (OAuth providers, SSO), asymmetric is the norm.
A correct verifier does all of the following:
alg is one you expect.kid).header.payload and compare it to the token's signature using a constant-time comparison.exp (not expired), nbf (active yet), iss (expected issuer), aud (intended for you).Skipping any step opens a hole.
alg: noneThe JWT spec includes a none algorithm meaning "unsigned". A naive library that honors it will accept a token with an empty signature. Never accept none. Always require a specific algorithm.
If a server verifies with "whatever the header says", an attacker can take a public RS256 key (which is public), craft a token with alg: HS256, and sign it using the public key as an HMAC secret. The server, trusting the header, verifies with that same public key and accepts the forgery.
The fix is to pin the algorithm in your verification call, independent of the token header:
// Node, using jsonwebtoken — pin the algorithm
import jwt from "jsonwebtoken";
const decoded = jwt.verify(token, publicKeyOrSecret, {
algorithms: ["RS256"], // never let the token choose
issuer: "https://auth.example.com",
audience: "my-api",
});
kidIf you pick the key from a kid header, only accept keys from a trusted JWKS you control — never fetch arbitrary URLs the token points to.
A valid signature on an expired token is still invalid for access. Enforce the temporal and scope claims:
const now = Math.floor(Date.now() / 1000);
if (payload.exp && now >= payload.exp) throw new Error("expired");
if (payload.nbf && now < payload.nbf) throw new Error("not yet valid");
if (payload.iss !== EXPECTED_ISSUER) throw new Error("bad issuer");
if (!toArray(payload.aud).includes(MY_AUDIENCE)) throw new Error("wrong audience");
Libraries do this when you pass issuer/audience options — use them rather than hand-rolling.
Server code must be the source of truth. A browser-based tool is for development and debugging: confirming a token was signed with the secret you think it was, checking why a token is rejected, or inspecting expiry during an auth bug hunt.
Prefer expired or throwaway test tokens, and never paste a production signing secret into any browser tool.
alg: none.exp/aud.== instead of a constant-time compare (timing side channel).Treat decode and verify as different operations. On the server, pin the algorithm, use the right key type, reject none, and validate exp/nbf/iss/aud on every request. Use browser tools only to debug with test credentials, and keep production secrets out of them.
A deep dive into JSON Web Token structure, signing algorithms, common vulnerabilities, and a safe workflow for decoding tokens in development without exposing production secrets.
Understand browser CORS, OPTIONS preflight, Access-Control-Allow-Origin, and credentials mode — plus the SPA misconfigurations that waste hours in production debugging.
How time-based one-time passwords work — shared Base32 secrets, 30-second windows, HMAC algorithms, clock skew, and why browser TOTP tools are for test secrets only.
Find a developer tool