JWT security: verifying claims without a framework
Your API just received a JWT, and the temptation is to reach for a framework’s auth middleware to check it. That works, but it hides what verification actually involves — and when your setup doesn’t fit the framework’s assumptions, you need to understand the checks yourself. The good news: a JWT is a signed, self-contained claim set, and verifying it correctly is a few dozen lines of code with the standard library.
What a JWT actually is
A JWT is three base64url segments: header, payload, signature. The header
names the signing algorithm, the payload carries claims like exp and
aud, and the signature binds the two together. Nothing about the token
is encrypted — the payload is readable by anyone who decodes it. Treat
every claim as public information and never put secrets inside.
The checks that matter
Order matters. Verify the signature before you trust any claim, using the public key of the issuer you actually trust. Then check, in order:
exp— reject expired tokens. This is the check people skip first and the one that causes the most real-world breaches.nbf— reject tokens used before their not-before time.aud— confirm the token was issued for your audience, not someone else’s service.iss— confirm the issuer matches the provider you configured.
If any check fails, reject the token. Do not partially accept claims from an unverified signature “for convenience” — that convenience is a vulnerability.
Algorithm confusion and other pitfalls
The classic attacks all involve trusting the header instead of your configuration:
alg: none— a token with no signature at all. Reject it outright; never accept unsigned tokens.- Algorithm confusion — the token says
HS256(symmetric) but you expectedRS256(asymmetric). A confused verifier can use the public key as the HMAC secret and forge tokens. Pin the expected algorithm instead of reading it from the header. kidinjection — the key ID header pointing at a value you never intended to use. Resolvekidagainst a fixed allowlist of keys.
The fix for all three is the same: verify against configuration you control, not against claims an attacker controls.
Verifying with the standard library
You don’t need a framework. In Go, crypto/rsa plus encoding/base64
covers signature verification, and time.Now() covers expiry — a
compact JWT validation function is roughly forty lines. Other languages
have small libraries that verify rather than frameworks that route:
jsonwebtoken in Node, PyJWT in Python, jose in Java.
That said, verification is only half the problem. The other half is where the token comes from and how it’s handled afterward. For an AI agent pipeline, the interesting question is whether the agent ever sees the raw token at all — a credential broker like https://github.com/ikkun1222/trustless can fetch and inject tokens without putting them in agent context.
Verification is a checklist, not a feature
Framework middleware is convenient, but it encourages treating JWT
security as a black box. The checks are simple and stable: pin the
algorithm, verify the signature with a key you control, then validate
exp, nbf, aud, and iss in that order. A small verifier you
understand beats a large one you trust by reputation.
What checks is your current JWT handling skipping — and could you write the verification by hand if you had to?