ProvenPin Install

Verify JWTs against a JWKS with jose (RS256, key rotation) in Vitest: working versions (tested 2026-10-15)

Verify third-party RS256 access tokens (Auth0, Cognito, Clerk style) with jose's JWKS key sets, checking issuer, audience, expiry and key rotation, tested offline with a local JWKS.

passed3 passed · 0 failed2026-10-15 11:54 UTC
Status
passed
Last passed
2026-10-15 11:54 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
jose@6.2.12@types/node@24.19.2vitest@5.0.4
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38730348195

The code

Exactly the files that passed. Your agent can pull them with get_recipe("jose-jwks-verify") (install), or see them on GitHub.

package.json
{
  "name": "jose-jwks-verify",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
  },
  "dependencies": {
    "jose": "6.2.12"
  },
  "devDependencies": {
    "@types/node": "24.19.2",
    "vitest": "5.0.4"
  }
}
src/verify.ts
import { jwtVerify, createLocalJWKSet, createRemoteJWKSet, type JSONWebKeySet, type JWTVerifyGetKey } from "jose";

// Production: createRemoteJWKSet(new URL("https://YOUR_ISSUER/.well-known/jwks.json")) caches and refetches on unknown kid.
export const remoteKeys = (jwksUrl: string) => createRemoteJWKSet(new URL(jwksUrl));
export const localKeys = (jwks: JSONWebKeySet) => createLocalJWKSet(jwks);

export async function verifyAccessToken(token: string, keys: JWTVerifyGetKey, issuer: string, audience: string) {
  const { payload } = await jwtVerify(token, keys, { issuer, audience, algorithms: ["RS256"], clockTolerance: 5 });
  return { sub: payload.sub!, scope: String(payload.scope ?? "").split(" ").filter(Boolean) };
}
test/verify.test.ts
import { describe, it, expect, beforeAll } from "vitest";
import { SignJWT, generateKeyPair, exportJWK, type CryptoKey } from "jose";
import { localKeys, verifyAccessToken } from "../src/verify";

const ISS = "https://auth.example.com/", AUD = "https://api.example.com";
let k1: { privateKey: CryptoKey; publicKey: CryptoKey }, k2: typeof k1, keys: ReturnType<typeof localKeys>;

beforeAll(async () => {
  k1 = await generateKeyPair("RS256"); k2 = await generateKeyPair("RS256");
  const jwk = async (k: CryptoKey, kid: string) => ({ ...(await exportJWK(k)), kid, alg: "RS256", use: "sig" });
  keys = localKeys({ keys: [await jwk(k1.publicKey, "old"), await jwk(k2.publicKey, "new")] }); // both published during rotation
});

const sign = (key: CryptoKey, kid: string, claims: Record<string, unknown> = {}, opts: { iss?: string; aud?: string; exp?: string } = {}) =>
  new SignJWT({ scope: "read:items write:items", ...claims }).setProtectedHeader({ alg: "RS256", kid })
    .setSubject("user_1").setIssuer(opts.iss ?? ISS).setAudience(opts.aud ?? AUD).setIssuedAt().setExpirationTime(opts.exp ?? "5m").sign(key);

describe("jwks verification", () => {
  it("accepts tokens from either published key", async () => {
    expect(await verifyAccessToken(await sign(k1.privateKey, "old"), keys, ISS, AUD)).toEqual({ sub: "user_1", scope: ["read:items", "write:items"] });
    expect((await verifyAccessToken(await sign(k2.privateKey, "new"), keys, ISS, AUD)).sub).toBe("user_1");
  });

  it("rejects wrong issuer, audience and expired tokens", async () => {
    await expect(verifyAccessToken(await sign(k1.privateKey, "old", {}, { iss: "https://evil/" }), keys, ISS, AUD)).rejects.toThrow();
    await expect(verifyAccessToken(await sign(k1.privateKey, "old", {}, { aud: "other" }), keys, ISS, AUD)).rejects.toThrow();
    await expect(verifyAccessToken(await sign(k1.privateKey, "old", {}, { exp: "-1m" }), keys, ISS, AUD)).rejects.toThrow(/exp/);
  });

  it("rejects a signature from the wrong key under a valid kid", async () => {
    await expect(verifyAccessToken(await sign(k2.privateKey, "old"), keys, ISS, AUD)).rejects.toThrow();
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.