JWK Sets
A JWK Set (RFC 7517 §5) is a JSON object with a keys array. It's the canonical way to publish multiple keys together — one for signing, several for rotation, a mix of algorithms — and to consume them as a single entity.
{
"keys": [
{ "kty": "RSA", "kid": "2025-q1", "alg": "RS256", "n": "...", "e": "AQAB" },
{ "kty": "RSA", "kid": "2025-q2", "alg": "RS256", "n": "...", "e": "AQAB" }
]
}
This is the format OAuth/OIDC providers publish at /.well-known/jwks.json. It's also how you'd persist your own rotating signing keys in a config file or database.
Consuming a set
Pass a JWK Set to verify() or decrypt() and the library handles the selection for you:
const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());
const { payload } = await verify(tokenFromProvider, jwks);
// ↑ unjwt picks the right key from the set based on the token's `kid`
Selection rules
When a JWKSet is passed (directly or returned from a lookup function):
Token carries a kid header — only keys with that exact kid are tried. Typically one match, so this is a fast O(1) path with no retry.
Token has no kid — every key in the set whose alg matches (or is compatible with) the token's alg is tried in order. The first one to verify successfully wins.
No matching candidates at all — throws JWTError("ERR_JWK_KEY_NOT_FOUND") before any crypto runs.
This is the mechanism behind transparent key rotation: add a new key to the set, retire the old one after a grace period, and your verification code never changes.
getJWKsFromSet
getJWKsFromSet(jwkSet, filter?)
Returns an array of JWKs from a set, optionally narrowed by a predicate:
import { getJWKsFromSet } from "unjwt/jwk";
const all = getJWKsFromSet(jwkSet);
// → all keys as JWK[]
const hmacOnly = getJWKsFromSet(jwkSet, (k) => k.kty === "oct");
// → only symmetric keys
const current = getJWKsFromSet(jwkSet, (k) => k.kid?.startsWith("2025-") ?? false);
// → only keys whose kid matches a prefix
Useful for:
- Picking the current signing key from a set that contains historical keys too.
- Building a multi-recipient JWE targeted at a specific subset of keyholders.
- Debugging — inspecting what's actually in a JWK Set.
getJWKFromSet (singular, deprecated) returns the first matching key. Prefer getJWKsFromSet — it composes with array methods and doesn't hide the "what if there are multiple matches?" question.Key rotation — a complete pattern
Issue with key A, which carries kid: "2025-q1". Publish your JWKS containing just A.
Generate key B (kid: "2025-q2"). Publish JWKS containing both A and B. Don't sign with B yet — verifiers need time to refresh their cache.
Cutover: start signing with B. Keep publishing both A and B so tokens still in flight validate.
Retire A: once A's longest-lived token has expired, remove A from JWKS. Only B remains.
Repeat.
On the verifier side, the same verify(token, jwks) call works through every step — no code changes. That's the whole point of JWK Sets.
Publishing a JWKS endpoint
If you're on the signing side and want downstream consumers to verify your tokens:
import { exportKey } from "unjwt/jwk";
// Keep private keys server-side only.
// Expose the PUBLIC half of each:
app.get("/.well-known/jwks.json", async () => {
return {
keys: [
await exportKey(currentPublicCryptoKey, { kid: "2025-q1", use: "sig" }),
await exportKey(previousPublicCryptoKey, { kid: "2025-q1", use: "sig" }),
],
};
});
In most apps you'd keep the JWK objects themselves in memory or a config store and hand them back — exporting from CryptoKey is only needed when you generated with generateKey() (non-JWK output) or imported opaquely.
Full example: Consuming a JWKS endpoint →.
Dynamic resolution — when you need more than a static set
If the set is large, changing frequently, or fetched per-request, use a JWKLookupFunction instead:
const { payload } = await verify(
token,
async (header) => {
const jwks = await jwksCache.get(header.iss); // cached fetch per issuer
return jwks; // return the whole set — library handles selection
},
{ algorithms: ["RS256", "ES256"] },
);