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 13:30 UTC- Status
- passed
- Last passed
- 2026-10-15 13:30 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 38742087302
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.
AWS SDK v3 DynamoDB document client and SQS events, tested with Vitest matchers
A single-table DynamoDB order service on @aws-sdk/lib-dynamodb with conditional puts, begins_with queries and optimistic locking, publishing SQS events, tested offline with aws-sdk-client-mock and aws-sdk-client-mock-vitest matchers.
bcryptjs password hashing + jose JWT sessions
Hash passwords with bcryptjs and issue and verify signed JWT session tokens with jose. Framework-agnostic auth core.
Effect 4 Schema classes, typed errors and retries, tested with @effect/vitest
Effect 4 Schema.Class models with checks, brands, FiniteFromString and fromJsonString for decode and encode, TaggedError failures, and Effect.retry with exponential backoff verified on the TestClock using @effect/vitest.