Signing and Verifying JWTs

A JWT is three base64url segments — header, payload, signature — and the first two are readable by anyone who holds the token. Decoding one tells you what it claims; only the third segment tells you whether to believe it. This article is about that third segment: how a signature is made, who can make one, how verifiers check it, and the exact point where "verified" stops meaning "trusted". You can run every step yourself in the JWT Signer & Verifier, which signs and verifies locally in your browser.

Decoding is not verifying

The most common misunderstanding about JWTs is also the most dangerous: that reading a token is the same as checking it. It is not. The header and payload are base64url — an encoding, not encryption — so anyone who intercepts a token, finds one in a log, or copies one from a browser's local storage can decode it in seconds, with no key at all. The claims inside (who it is for, when it expires, what roles it asserts) are assertions, and assertions are cheap.

The signature exists precisely because the claims are cheap. It is a cryptographic binding between the header, the payload, and a key only the issuer holds: change one character of the payload and the signature stops matching. Verification is the act of replaying that maths — recomputing the signature over the header and payload you received, with the issuer's key, and comparing. If it matches, the token is exactly as the issuer signed it; if it does not, something changed, and the token is worthless no matter how plausible its claims look.

Symmetric vs asymmetric: who can mint a token

With HS256, the secret that signs is the same secret that verifies. That makes HMAC fast, simple, and entirely adequate inside one service — your API signs its own tokens and checks them itself, and the secret never leaves the building. The moment a second party needs to verify, the model breaks: handing them the secret hands them the power to mint tokens, because verification and signing are the same operation. A leaked HMAC secret is not a verification problem; it is a forgery problem.

Asymmetric algorithms split the two powers. RS256, ES256 and EdDSA sign with a private key and verify with a public key, so the issuer can publish its public keys — as a JWKS — and every verifier in the world can check tokens without ever holding the power to forge one. That split is what makes JWTs work across services: your API trusts tokens from an identity provider because it trusts that provider's published keys, not because it shares a secret with it. The trade is complexity — key formats, rotation, kid matching — which is what the signer and verifier on this site exists to make concrete.

The algorithm-confusion attack

The classic JWT attack needs no broken cryptography — it needs a verifier that trusts the token to name its own algorithm. The attacker takes a legitimate RS256 token, whose public key is published (as public keys are), and re-signs the same payload with HS256, using the public key itself as the HMAC secret. A verifier that reads alg from the header and picks its verification routine accordingly will now "verify" the forged token with HMAC, using the public key as the secret — and the public key is public.

The defence is structural, not advisory: the verifier must pin the algorithms it accepts, and the key family must match the algorithm family — an RSA key can never verify an HMAC token, and a secret can never verify an RSA token. A verifier that reinterprets a key across families is broken by design, however correct its cryptography is. This is why the verifier on this site refuses cross-family keys with an explicit warning instead of attempting the check: the refusal is the feature.

What JWKS and kid rotation look like in practice

An identity provider does not hand out one eternal key; it publishes a set — a JWKS, usually at /.well-known/jwks.json — and rotates keys by adding a new one and, after a grace period, removing the old. Each token's header carries a kid naming the key that signed it, so a verifier fetches the set, picks the matching key, and verifies. Tokens signed with the old key keep working until it is removed, which is what makes rotation safe instead of a flag day.

The operational details are where it breaks. Verifiers must cache the JWKS (fetching it per request is a self-inflicted outage), handle the case where a kid is unknown — usually a rotation the cache has not seen yet, so re-fetch before failing — and never accept a key set from an untrusted URL: the set is only as trustworthy as the channel it came over, which is why JWKS endpoints live behind HTTPS and why the JWKS builder here derives each kid as an RFC 7638 thumbprint of the key itself, so the id cannot be spoofed independently of the key.

exp, nbf, iat — and the clock problem

A signature proves a token was signed; it says nothing about whether it should still be honoured. That is the job of the time claims: exp (when it stops being valid), nbf (when it starts), iat (when it was issued). A verifier that checks the signature and skips the clock accepts a token forever — including one stolen a year ago. Short lifetimes are the practical answer: access tokens measured in minutes, refresh tokens carrying the long-lived trust, and revocation handled by simply not re-issuing.

Clocks disagree, so verifiers allow a little slack — typically a minute or two of leeway on exp and nbf — but leeway is a tolerance for drift, not a licence to ignore expiry. A device whose clock is minutes out will produce tokens its own server rejects, and a token with an iat in the future is a forgery signal, not a rounding error. The same clock discipline applies to the tooling: the verifier checks expiry against the clock you can see, and says plainly when a token is expired rather than merely "invalid".

What "verified" does and does not prove

A verified token proves three things and nothing more: the header and payload are exactly as signed, the signer held the key, and the token is inside its validity window. It does not prove the issuer is who you think it is — that depends on where the key came from, and a key fetched from an attacker-controlled URL verifies an attacker's tokens perfectly. It does not prove the claims are true — a signature authenticates the statement, not the facts behind it. And it does not prove the token was not stolen: a valid token presented by anyone is accepted, which is why tokens are short-lived and scoped.

So the trust decision lives outside the token. Your API trusts a token because it fetched the key from its identity provider's well-known URL over HTTPS, because the iss matches the provider it configured, and because the aud matches the service it is. Verification is the last check, not the first: key provenance, issuer, audience, then signature, then time. A tool that reports "VALID" without that context is telling the truth about the maths and nothing about the trust — which is why this site's verifier says so on the page, next to the verdict.

Frequently asked questions

Can I verify a token without the key?

No — verification needs the issuer's public key (or the shared secret, for HMAC). Decoding needs no key, which is exactly the confusion to avoid: a decoded token is readable, not verified. If the issuer publishes a JWKS, fetch it from the well-known URL and match the token's kid; the verifier on this site does that for you.

Why does my RS256 token fail with my secret?

Because a secret and an RSA key are different families, and a sound verifier refuses to mix them. RS256 tokens are verified with the issuer's RSA public key — PEM, JWK or JWKS — never with a text secret. Using a secret would be the algorithm-confusion attack: the public key is public, so anyone could forge tokens if HMAC verification accepted it as the secret.

How long should a JWT live?

Minutes, not days, for access tokens: 5–15 minutes is the common range, with a refresh token carrying the longer session. A short lifetime bounds the damage of a stolen token to the window before it expires, and makes revocation a matter of not re-issuing rather than maintaining a blocklist. Tokens that live for weeks are session cookies with worse tooling.

Sources