JWT Verification

Verify access tokens locally and refresh JWKS on key rotation
View as Markdown

Verify Gwop access tokens locally with jose. Do not call the API on every request just to check a signature.

Why this pattern

  • JWT signature verification stays local and fast
  • Gwop JWKS is fetched on cold start
  • the verifier refreshes JWKS only when a token carries an unknown kid
  • sid is preserved so the app can perform a live session lookup when revocation matters
1import {
2 createLocalJWKSet,
3 decodeProtectedHeader,
4 jwtVerify,
5} from "jose";
6
7let cachedJwks: {
8 keys: Array<Record<string, unknown> & { kid?: string }>;
9 getKey: ReturnType<typeof createLocalJWKSet>;
10} | null = null;
11
12async function refreshJwks() {
13 const { result } = await gwop.auth.getJwks(identityRequestOptions);
14 const keys = result.keys as Array<Record<string, unknown> & { kid?: string }>;
15
16 cachedJwks = {
17 keys,
18 getKey: createLocalJWKSet({ keys }),
19 };
20
21 return cachedJwks;
22}
23
24async function getJwksForToken(token: string) {
25 const header = decodeProtectedHeader(token);
26
27 if (!cachedJwks) {
28 return refreshJwks();
29 }
30
31 if (
32 typeof header.kid === "string" &&
33 !cachedJwks.keys.some((key) => key.kid === header.kid)
34 ) {
35 return refreshJwks();
36 }
37
38 return cachedJwks;
39}
40
41async function verifyAccessToken(token: string) {
42 const jwks = await getJwksForToken(token);
43 const { payload } = await jwtVerify(token, jwks.getKey, {
44 issuer: "https://identity.gwop.io",
45 algorithms: ["RS256"],
46 });
47
48 return {
49 subject: payload.sub,
50 sessionId: payload.sid,
51 };
52}

What this does not replace

Local JWT verification proves signature, issuer, audience, and expiry. It does not replace a live session lookup if you need to reject revoked sessions immediately. Keep sid so you can pair local verification with gwop.authSessions.get() when needed.