# Consuming a JWKS endpoint

> 

OAuth and OIDC providers (Auth0, Okta, Entra, Keycloak, GitHub, Google, …) publish their public keys as a **JWKS** — a JWK Set served from a well-known URL:

```text
https://auth.example.com/.well-known/jwks.json
```

Your service fetches that document, caches it, and uses it to verify tokens issued by the provider. Each token's header carries a `kid` that matches one of the keys in the set.

## The one-shot version

```ts [verify.ts]
import { verify } from "unjwt/jws";

const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());

const { payload } = await verify(tokenFromProvider, jwks, {
  algorithms: ["RS256", "ES256"],
  issuer: "https://auth.example.com",
  audience: "my-api",
});
```

unjwt reads the token's `kid`, finds the matching key in the set, and verifies. If `kid` is absent or doesn't match, it throws `ERR_JWK_KEY_NOT_FOUND` before any crypto runs.

<warning>

**Always pass algorithms** when consuming third-party tokens. The library's inferred allowlist from a set of mixed-algorithm keys is broader than you probably want — being explicit stops a malicious issuer from downgrading to a weaker algorithm you'd otherwise accept.

</warning>

## The production version — cached fetch

In a real service you'd cache the JWKS to avoid hitting the provider on every request:

```ts [jwks-cache.ts]
interface JWKSCacheEntry {
  jwks: { keys: JWK[] };
  fetchedAt: number;
}

const jwksCache = new Map<string, JWKSCacheEntry>();
const JWKS_TTL_MS = 60 * 60 * 1000; // 1 hour

async function getJwks(issuer: string): Promise<{ keys: JWK[] }> {
  const cached = jwksCache.get(issuer);
  if (cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) {
    return cached.jwks;
  }

  const url = new URL("/.well-known/jwks.json", issuer).toString();
  const jwks = await fetch(url).then((r) => {
    if (!r.ok) throw new Error(`JWKS fetch failed: ${r.status}`);
    return r.json();
  });

  jwksCache.set(issuer, { jwks, fetchedAt: Date.now() });
  return jwks;
}
```

Then verify via a lookup function so unjwt only pays the fetch cost when the `kid` isn't already cached:

```ts [verify-with-cache.ts]
import { verify } from "unjwt/jws";
import type { JWKLookupFunction } from "unjwt";

const ISSUER = "https://auth.example.com";

const lookup: JWKLookupFunction = async (header) => {
  const jwks = await getJwks(ISSUER);
  return jwks; // unjwt picks the matching kid from the set
};

const { payload } = await verify(tokenFromProvider, lookup, {
  algorithms: ["RS256"],
  issuer: ISSUER,
  audience: "my-api",
});
```

## Handling key rotation

When the provider rotates keys, the new key gets added to the JWKS and the old one sticks around for a grace period. Your service should:

<steps level="4">

#### **Refresh the cache when an unknown kid is seen** — don't wait for the TTL.

#### **Keep serving requests** during the refresh; old tokens may still be in flight.

</steps>

A pattern for on-demand refresh:

```ts [jwks-smart-cache.ts]
const lookup: JWKLookupFunction = async (header) => {
  let jwks = await getJwks(ISSUER);

  // Fast path: kid is already in the cached set
  if (header.kid && jwks.keys.some((k: any) => k.kid === header.kid)) {
    return jwks;
  }

  // Unknown kid — bypass cache and re-fetch once
  jwksCache.delete(ISSUER);
  jwks = await getJwks(ISSUER);
  return jwks;
};
```

Be careful not to loop if the fetched JWKS *still* doesn't have the `kid` — rate-limit the re-fetch (e.g. allow at most one per minute per issuer) to avoid a cache-busting attack where a malicious party sends tokens with random unknown `kid`s.

## Short example with a rate-limit

```ts [rate-limited-refresh.ts]
const LAST_REFRESH_FLOOR_MS = 60_000; // don't re-fetch more than once/minute per issuer
const lastRefresh = new Map<string, number>();

async function getJwksFresh(issuer: string) {
  const now = Date.now();
  const last = lastRefresh.get(issuer) ?? 0;
  if (now - last > LAST_REFRESH_FLOOR_MS) {
    jwksCache.delete(issuer);
    lastRefresh.set(issuer, now);
  }
  return getJwks(issuer);
}
```

## What exactly unjwt does

With `verify(token, jwks)`:

<steps level="4">

#### Decodes and validates the protected header.

#### Reads the token's `kid`.

#### Looks up candidates in `jwks.keys`:
- If `kid` is present, **only keys with matching kid** are candidates (usually one).
- If `kid` is absent, every key whose `alg` is compatible with the token is a candidate.



#### Verifies against the first candidate; falls back to the next on failure.

#### If no candidate verifies, throws.

</steps>

No retry logic on your side, no `kid` parsing on your side.

## See also

- [JWK Sets →](/jwk/jwk-sets)
- [JWS verifying → JWKSet automatic rotation](/jwt/jws/verifying#jwkset-automatic-key-rotation)
- [Authentication basics →](/examples/authentication-basics) — for the single-service case.
