Overview
A JSON Web Token carries signed claims. The resource server must treat the token as untrusted input until every check passes. Validation belongs in one shared library or gateway filter, not in each handler.
A good rollout has one internal library or gateway plugin used by every service, with conformance tests that feed it forged, expired, wrong-audience and unsigned tokens. Metrics on rejection reasons quickly reveal clock skew problems, rotation incidents and probing attacks, so expose them on dashboards.
How it works
token = header.payload.signature
1. parse header; alg must be in allow-list; reject none
2. key = jwksCache.get(header.kid) # refetch once if missing
3. verify signature(key, header.payload)
4. require iss == expectedIssuer
5. require aud contains thisApi
6. require now < exp (+ small clock skew) and now >= nbf
7. authorize: scope / role claims vs route policy
Successful validation yields a principal object passed to business code; raw tokens are not re-parsed downstream.
Pitfalls
- Trusting the alg header instead of a configured allow-list.
- Using decode functions that skip verification.
- Ignoring aud, so a token for service A works on service B.
- Unlimited JWKS refetching on unknown kid, which lets attackers trigger a flood of outbound calls.
- Putting PII or secrets in the payload; it is only encoded, not encrypted.
- Long expiry with no revocation path.
Key takeaways
- Pin algorithms, issuer and audience; never trust the token header.
- Cache JWKS, handle rotation, and rate-limit refetches.
- Keep lifetimes in minutes and add introspection for sensitive actions.
- Centralize validation so fixes apply everywhere.