# JWK Sets

> 

A **JWK Set** ([RFC 7517 §5](https://www.rfc-editor.org/rfc/rfc7517#section-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.

```json
{
  "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:

```ts [from-endpoint.ts]
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](/jwt/jws/verifying#dynamic-key-resolution-jwklookupfunction)):

<steps level="4">

#### **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**.

</steps>

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`

```ts
getJWKsFromSet(jwkSet, filter?)
```

Returns an array of JWKs from a set, optionally narrowed by a predicate:

```ts [filter.ts]
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.

<warning>

`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.

</warning>

## Key rotation — a complete pattern

<steps level="4">

#### **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.

</steps>

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:

```ts [jwks-endpoint.ts]
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 →](/examples/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`](/jwt/jws/verifying#dynamic-key-resolution-jwklookupfunction) instead:

```ts [dynamic.ts]
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"] },
);
```

## See also

- [Verifying → JWKSet retry](/jwt/jws/verifying#jwkset-automatic-key-rotation)
- [Examples: JWKS endpoint](/examples/jwks-endpoint)
- [Adapters: key rotation via hooks](/adapters/hooks)
