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
npm install --no-audit --no-fund --ignore-scripts=false npm test
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.
{
"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"
}
}
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) };
}
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.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
bcryptjs password hashing + jose JWT sessions
Hash passwords with bcryptjs and issue and verify signed JWT session tokens with jose. Framework-agnostic auth core.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
Hono + Valibot request validation (Vitest)
Validate JSON bodies and query strings in Hono with Valibot schemas via @hono/valibot-validator, with custom error responses, tested with Vitest.