unit U3 — 3 of 4
JWT verification
JWKS, clock skew, audience checks
A JWT lets a caller prove who they are without a database lookup — the token itself carries signed claims. But a JWT is only trustworthy after you verify its signature; the decoded payload is otherwise just base64 that anyone can craft. When a partner posts a signed token to your Webhook trigger, verification is the gate that decides whether the request is really from them. Skip it and you are trusting a string the caller handed you — the automation equivalent of checking an ID card without looking at it.
Modern providers sign with rotating keys and publish the public halves at a JWKS endpoint. Read the token header, match its kid to a key in the JWKS document, and verify the signature against that key — caching the key set so you are not fetching it on every request. Then check the standard claims that actually gate access: iss must be the expected issuer, aud must name your service, and exp/nbf must place now inside the valid window. A valid signature on a token minted for someone else is still a rejection.
Where it breaks: clock skew. Your server clock and the issuer’s are never perfectly aligned, so a token that is genuinely valid can look expired — or not yet valid — by a second or two. Allow a small tolerance, typically 30 to 60 seconds, when comparing exp and nbf. Too tight and you reject good traffic in bursts around expiry; too loose and you quietly extend the useful life of a stolen token. Tune it, and log every rejection reason.
worked example
Verifying a partner’s inbound JWT in a Code Piece — JWKS lookup, audience and skew checks included.
import { createRemoteJWKSet, jwtVerify } from 'jose';
export const code = async (inputs) => {
const token = inputs.headers.authorization.split(' ')[1]; // strip "Bearer "
const JWKS = createRemoteJWKSet(
new URL('https://auth.partner-co.dev/.well-known/jwks.json'),
);
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.partner-co.dev/',
audience: 'https://hooks.youragency.dev/intake',
clockTolerance: 45, // seconds of skew
});
return { sub: payload.sub, scope: payload.scope };
};field checklist
- Verify the signature before reading any claim in the token.
- Match the token kid against a cached JWKS key set.
- Assert iss and aud match your expected issuer and service.
- Allow 30–60 seconds of clock skew on exp/nbf.
- Reject tokens missing kid, aud, or a valid signature.
common failure — Accepting a token meant for another service
A flow verified the JWT signature, saw it was cryptographically valid, and let the request through. The token was genuine — but issued for a different service the same provider also signs, so any of that provider’s customers could call the endpoint. A valid signature proves who signed the token, not who it was for. Always assert the aud claim names your service before trusting a single other claim.
check your understanding
A flow verified a JWT signature, found it cryptographically valid, and let the request through — but the token had been issued for a different service the same provider signs. Which claim must be asserted to close this?
next unit opens once this is passed
sandbox validation
The check above confirms you followed the unit. Marking the module COMPLETED takes more: build the automation in your own engine and submit the exported flow and its run evidence, signed, to your unique validation URL. See the module page for that spec.