import { createRemoteJWKSet, jwtVerify } from "jose"; // Fetched on first use, cached, and fetched again when a token names an unknown kid// (at most once per 30 seconds)const jwks = createRemoteJWKSet(new URL("https://issuer.example.com/.well-known/jwks.json")); export async function verifyAccessToken(token) { const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], // never let the token choose the algorithm issuer: "https://issuer.example.com", audience: "api://orders", requiredClaims: ["exp"], // exp is checked when present; this makes it mandatory }); return payload;}import { readFileSync } from "node:fs"; for (const line of readFileSync("tokens.txt", "utf8").trim().split("\n")) { const [label, token] = line.split(" "); try { const claims = await verifyAccessToken(token); console.log(`${label}: accepted, sub=${claims.sub}`); } catch (err) { console.log(`${label}: rejected: ${err.code}: ${err.message}`); }}How it works
createRemoteJWKSet(url)returns a key-resolver function. The set is fetched on first use, cached, and fetched again when a token names akidthe cache does not have.jwtVerifyselects the JWK whosekid,kty,crvandalgfit the token, and verifies the signature before looking at any claim.algorithms,issuerandaudienceturn the checks on;requiredClaims: ["exp"]also rejects tokens without an expiry.- Each failure has its own
code—ERR_JWT_CLAIM_VALIDATION_FAILED,ERR_JWT_EXPIRED,ERR_JWKS_NO_MATCHING_KEY— so the caller can map them to 401 responses or logs.
Gotchas
expis only checked when present: a token without it passesjwtVerifyunlessrequiredClaimsincludes"exp". Issuers usually set it; an attacker-minted token signed by a leaked key does not have to.- Without
algorithms, the remote key set still refuses anHS256token (ERR_JOSE_NOT_SUPPORTED: Unsupported "alg" value for a JSON Web Key Set), but pinning the expected algorithm is the documented defence — keep it explicit. - A token without
kidagainst a set with two matching keys fails withERR_JWKS_MULTIPLE_MATCHING_KEYS; jose does not try the keys one by one. Issuers that rotate keys must setkid. - The unknown-kid refetch has a 30-second cooldown (
cooldownDuration), so a flood of tokens with inventedkidvalues cannot turn into a flood of requests to the issuer.