Understanding JWT Signature Verification
Plenty of developers know how to decode a JWT. Fewer know exactly what the signature proves, or why different algorithms need to be verified in completely different ways. This closes that gap.
What the signature proves โ and what it doesn't
A JWT's header and payload are Base64URL-encoded, not encrypted. Anyone can decode and read them. The signature guarantees exactly one thing: "this header and payload haven't been altered since someone holding this key signed them." It doesn't hide the content, and it doesn't vouch for the issuer's trustworthiness. Keep that in mind โ it's the reason the vulnerabilities below are dangerous.
Why HS256 and RS256/ES256 verify so differently
JWT signature algorithms fall into two families.
- Symmetric (HS256/384/512, HMAC) โ signing and verification use the same secret key. Issuer and verifier must share that secret, which fits setups where one server (or a small trusted group) does both issuing and verifying โ a self-contained login system, for example.
- Asymmetric (RS256/384/512, PS256/384/512, ES256/384/512) โ the issuer signs with a private key, and verifiers only need the public key. A leaked public key isn't a problem, which is why this family fits microservices or third-party clients that only ever need to verify โ OAuth/OIDC ID tokens, for example.
The code that actually does this in a browser is shorter than you'd expect. HMAC verification looks like this:
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
const valid = await crypto.subtle.verify(
"HMAC", key, signatureBytes, new TextEncoder().encode(`${header}.${payload}`)
);
For public-key algorithms like RS256/ES256, the only real difference is passing "spki" plus the bytes extracted from a PEM to importKey, then verifying with RSASSA-PKCS1-v1_5 or ECDSA. ECDSA has an interesting quirk: the JOSE (JWT) spec encodes the signature as fixed-length r and s concatenated together, and that happens to match exactly what the Web Crypto API expects โ no conversion needed.
Nymphsoft's JWT Decoder implements exactly this logic โ paste a token, enter the secret or public key, and it verifies right in your browser. The key is never sent to a server.
Production vulnerabilities involving alg
1. The alg: none attack
The JWT spec also defines alg: "none" โ no signature at all. If a verification implementation blindly trusts the header's alg to decide how to verify, an attacker can set alg to none, leave the signature empty, and have the server treat it as "no signature to check" and accept it. The fix is simple: the server must whitelist the algorithm it expects and always reject none.
2. Algorithm confusion attacks
A subtler case. Say a service normally issues tokens with RS256 (asymmetric), and its verification code also reads alg from the header to decide how to verify. RS256's public key is, by definition, public โ anyone can get it. An attacker changes the token's alg to HS256 and forges a signature using that public key string as the HMAC secret. If the verifying server sees "alg is HS256" and treats the public key as if it were an HMAC secret, the forged signature passes. This vulnerability was found in several major JWT libraries back in 2015 and became widely known โ and it resurfaces any time an implementation trusts alg dynamically at verification time. The defense is the same principle: server configuration decides the algorithm, not the token.
This is why mature JWT libraries (like jsonwebtoken or jose) require you to explicitly pass allowed algorithms to verify(), e.g. algorithms: ["RS256"]. Never leave that option out or empty.
Why browser-side verification isn't enough on its own
Being able to verify a signature with Nymphsoft's JWT tool doesn't make client-side verification a substitute for real authentication. Checking a signature in the browser is useful for debugging and learning, but actual authentication/authorization decisions must happen on the server (or a backend you trust). The reason is simple: client-side code runs in an environment the user fully controls, so a "verification result" the client reports about itself can always be tampered with.
Production checklist
- Pin the allowed
algin server configuration โ never trust the token header's value. - Always reject
alg: none. - Check
exp(expiration) andnbf(not-before) on every request, not just the signature. - If you have multiple issuers or audiences, verify
issandaudtoo. - With asymmetric keys, support key rotation via the
kid(Key ID) header, and reject revokedkidvalues. - Use a well-tested JWT library instead of rolling your own signing/verification logic.
Frequently Asked Questions
Why use JWT instead of sessions?
Sessions require the server to store state, while a JWT carries what it needs inside the token itself, so verifying the signature is enough โ no server-side storage required (stateless). The trade-off is that an issued JWT is hard to revoke immediately before it expires.
Should I use a symmetric key (HS256) or asymmetric (RS256)?
If the same server (or a small group of fully trusted servers) issues and verifies tokens, HS256 is fine. If multiple microservices or third-party clients only need to verify โ without ever needing the ability to issue โ RS256/ES256 is safer since it avoids sharing a secret.
Can I use Nymphsoft's JWT tool's verification result for authentication?
No. This tool is for debugging and learning. Real authentication/authorization must happen server-side, with a pinned algorithm whitelist and a vetted library.