1const JWKS_URL = "https://issuer.example.com/.well-known/jwks.json";
2const ISSUER = "https://issuer.example.com";
3const AUDIENCE = "api://orders";
4
5const b64urlBytes = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
6const b64urlJson = (s) => JSON.parse(new TextDecoder().decode(b64urlBytes(s)));
7
8let keySet; // cached JWKS
9async function findKey(kid) {
10 for (const refresh of [false, true]) { // one refetch for an unknown kid
11 if (!keySet || refresh) keySet = await (await fetch(JWKS_URL)).json();
12 const jwk = keySet.keys.find((k) => k.kid === kid && k.kty === "EC" && k.crv === "P-256" && (k.use ?? "sig") === "sig");
13 if (jwk) return crypto.subtle.importKey("jwk", jwk, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
14 }
15 throw new Error(`no key with kid "${kid}" in the JWKS`);
16}
17
18async function verifyAccessToken(token) {
19 const [h, p, s] = token.split(".");
20 const header = b64urlJson(h);
21 if (header.alg !== "ES256") throw new Error(`unexpected alg ${header.alg}`); // pin the algorithm
22 const key = await findKey(header.kid);
23 const ok = await crypto.subtle.verify(
24 { name: "ECDSA", hash: "SHA-256" }, key, b64urlBytes(s), new TextEncoder().encode(`${h}.${p}`),
25 );
26 if (!ok) throw new Error("invalid signature");
27
28 const claims = b64urlJson(p);
29 const now = Math.floor(Date.now() / 1000);
30 if (claims.iss !== ISSUER) throw new Error(`unexpected iss ${claims.iss}`);
31 if (![].concat(claims.aud).includes(AUDIENCE)) throw new Error(`unexpected aud ${claims.aud}`);
32 if (typeof claims.exp !== "number" || claims.exp <= now) throw new Error(`expired at ${claims.exp}`);
33 return claims;
34}