Auth & Security63 min total · 19 parts
OAuth 2.0 and JWT Explained: What Really Happens When You Click "Sign In"
Part 10 of 19 · ~3 min
JWT Signing Algorithms: HMAC vs. RSA/ECDSA
That alg value in the header isn't decoration — it names the actual cryptographic math backing the signature, and picking one family over the other changes what's even possible once more than one of Tasklight's services needs to check a token on its own, without asking anyone else for permission.
HMAC (HS256, HS384, HS512) relies on exactly one key, and that same key does both jobs — producing a signature and confirming one. That's a fine arrangement right up until it isn't — for a lone backend checking its own tokens, there's nothing to complain about. Hand that same key to a second, third, and fourth service so each can verify tokens too, and you've just handed all four of them the power to mint a convincing fake, because verifying and forging both reduce to "do you have the key."
RSA and ECDSA (RS256, ES256, and their relatives) break that key in two. Signing takes the private half, which in Kestrel's case never leaves Kestrel's own infrastructure; verifying takes the public half, which is meant to be handed out freely. That's exactly the shape Tasklight needs: tasks-service and billing-service each pull Kestrel's public key from a JWKS endpoint — https://id.kestrel.example/.well-known/jwks.json — and can check as many tokens as they want, forever, without either one holding a key that could produce a forged one.
| HMAC (HS256) | RSA/ECDSA (RS256/ES256) | |
|---|---|---|
| Key type | One shared secret | Private key signs, public key verifies |
| Who can verify | Anyone holding the shared secret | Anyone with the public key |
| Who can forge a token | Anyone who can verify it | Only the holder of the private key |
| Fits | One backend, issuing and checking its own tokens | One issuer, several independent verifiers — exactly Kestrel's situation |
That same split between signing and verifying keys turns out to be where a genuinely well-known attack comes from. Early on, before tasks-service's verifier pinned its expected algorithm, it trusted whatever alg the token claimed to use. Here's the forgery that makes possible: grab any legitimately RS256-signed Kestrel token, delete its signature, rewrite the header to say HS256 instead, and produce a new signature over the result — treating Kestrel's own public key, freely downloadable from the JWKS endpoint by design, as though it were a shared HMAC secret. Walk through what actually crosses the wire: an attacker fetches Kestrel's public key from https://id.kestrel.example/.well-known/jwks.json — no authentication required, it's meant to be public — takes any RS256 token they've ever legitimately seen (even one of their own, scoped to nothing interesting), swaps its header to {"alg":"HS256","typ":"JWT"}, edits the payload to claim whatever scope they'd like, and computes an HMAC-SHA256 signature over the result using the downloaded public key as the HMAC secret. A verifier configured to "accept whatever the token says its own algorithm is" checks that signature the HMAC way, sees it match, and waves the forgery straight through as a validly signed token with any claims the attacker chose. The fix, covered next, is that the verifier decides the algorithm — never the token.