Paste a JSON Web Token into any decoder and you can immediately read the user ID, roles and expiry inside it. That surprises many developers the first time: if anyone can read it, how can it be secure? The answer is that a signed JWT is designed to be readable by everyone but changeable by no one. Decoding reads it; verification is what proves it has not been changed.
What is inside a JWT
A signed JWT in compact form looks like this (line breaks added):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyXzEwMjQiLCJyb2xlcyI6WyJhZG1pbiJdLCJleHAiOjE3NjcyMjkyMDB9
.<signature>The three parts are:
- Header — base64url-encoded JSON describing the token, e.g.
{"alg":"HS256","typ":"JWT"}. - Payload — base64url-encoded JSON claims, e.g.
{"sub":"user_1024","roles":["admin"],"exp":1767229200}. - Signature — computed over the first two parts with a secret or private key.
Base64url is an encoding, not encryption. It exists so that binary data and JSON can travel safely in HTTP headers and URLs. Reversing it requires no key, which is why the JWT decoder can show the contents instantly and entirely in your browser.
What decoding tells you
- Which claims the token says it contains.
- Which algorithm and key ID the token says it was signed with.
- Whether the
exptime has passed according to your clock — handy when debugging a 401 response.
Every one of those statements is a claim made by whoever produced the token. Without checking the signature, nothing stops someone from editing the payload — changing "roles":["user"] to "roles":["admin"] — and re-encoding it. The decoded result would look perfectly normal.
What verification does
Verification recomputes the signature over the received header and payload using the expected key, and checks that it matches the signature in the token. If even one character of the payload changed, the check fails. A complete verification also validates the claims:
- Algorithm: accept only the algorithm(s) you expect. Never let the token's own
algheader choose, and never acceptnone. - Key: use the issuer's published key (often from a JWKS endpoint) or your shared secret.
- Signature: recompute and compare.
- Time claims: reject if
exphas passed ornbfis in the future, allowing a small clock-skew margin. - Issuer and audience: check that
issis the issuer you trust andaudincludes your service.
Verifying in practice
Use a maintained library rather than writing signature checks yourself:
// Node.js with the "jose" package (MIT licence)
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://auth.example.com/.well-known/jwks.json"));
const { payload } = await jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "api.example.com",
algorithms: ["RS256"],
});# Python with PyJWT (MIT licence)
import jwt
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="api.example.com",
issuer="https://auth.example.com",
)Both libraries raise an error if the signature, expiry, issuer or audience check fails. Note that PyJWT's algorithms argument is required for exactly the reason described above.
Common mistakes
- Trusting decoded claims in the browser. Front-end code can decode a token to show the user's name, but every authorization decision must happen on a server that verifies the token.
- Putting secrets in the payload. Anyone holding the token can read it. Do not include passwords, API keys or personal data you would not show the user.
- Confusing seconds and milliseconds.
expis in seconds. Comparing it with JavaScript'sDate.now()(milliseconds) without dividing by 1000 makes every token look expired. See Unix timestamps explained. - Using a decode-only function for authentication. Many libraries offer a
decode()helper alongsideverify(). The decode helper skips all security checks.
Is it safe to paste a token into an online decoder?
It depends on where the decoding happens. A decoder that runs entirely in your browser, like the one on this site, never transmits the token. A decoder that sends the token to a server gives that server a working credential until the token expires. Either way, prefer test or expired tokens, and revoke a production token if you are unsure where it went.