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 expected RS256 (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.
  • kid injection — the key ID header pointing at a value you never intended to use. Resolve kid against 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?