JWT & key confusion showed you
two verifiers side by side: a naive one that trusts whatever the token's own header claims,
and a safe one that never lets the token choose how it gets checked. This exercise is the
safe one — verify(token, keys, allowed_algs) — built from scratch against 8
checks, four of which are the exact attacks the lesson walked through.
verify(token: str, keys: dict, allowed_algs: set) -> dict — a compact JWT
verifier. keys maps kid → Key (a Key has a
kind of "hmac" or "rsa", plus secret or
public_pem). allowed_algs is the set of algorithms the
caller accepts — never read that decision from the token. Return the decoded payload
dict on success; raise the provided JWTError with a specific reason otherwise.alg=none, unconditionally, before anything else runs. An
algorithm whose family doesn't match the located key's own kind — RS256 header
over an HMAC key or vice versa. An unknown kid (looked up as an exact key into
keys, never split, normalized, or path-resolved). An expired token
(exp) or one not yet valid (nbf). A signature compared with
anything but hmac.compare_digest._rsa_verify_stub(signature, signing_input, public_pem) is provided —
a deterministic stand-in for real RSA verification, since no RSA library ships to the
browser here. Call it for the RS256 branch; do not reimplement or inline its logic.The checks are ordinary Python and ship with the page like everything
else on a static site — open devtools and you can read every one. Check 3 signs a token with
the RSA public key used as an HMAC secret: a verifier that dispatches purely on the header's
alg (never checking the key's own kind) accepts it — that's the
check that actually catches key confusion, not just "wrong signature".
alg == "none" (and alg not in allowed_algs)
before you ever look up kid. Order matters: reject the algorithm first, always.Key for kid, check
key.kind matches the algorithm family ("HS" → "hmac",
"RS" → "rsa") before you use key.secret or
key.public_pem for anything. Never let a value typed for one purpose (a public
key meant for RSA) get used for another (an HMAC secret) just because the header asked.keys.get(kid), exactly. No .split("/"), no
stripping "..", no "be lenient" fallback. kid is an opaque string,
not a path.payload.get("exp") must be strictly greater than
time.time(); payload.get("nbf"), if present, must not be in the
future. Both are optional in the JWT spec (skip the check when the claim is absent) but
required whenever they are present in the token this exercise hands you.hmac.compare_digest(signature, expected), never
signature == expected. The check reads which function your verify
actually calls, not just whether accept/reject comes out right.= padding on the wire; pad
back out to a multiple of 4 before base64.urlsafe_b64decode, or a segment
whose length isn't already a multiple of 4 raises binascii.Error.