# `ConduitMcp.OAuth.KeyProvider.JWKS`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/oauth/key_provider/jwks.ex#L2)

JWKS key provider that fetches keys from an HTTP endpoint with ETS caching.

Fetches JSON Web Key Sets from the authorization server's JWKS URI,
caches them in ETS, and auto-refreshes on cache miss or expiration.

## Configuration

    auth: [
      strategy: :oauth,
      key_provider: {ConduitMcp.OAuth.KeyProvider.JWKS,
        jwks_uri: "https://auth.example.com/.well-known/jwks.json",
        cache_ttl: :timer.hours(1)}    # default: 1 hour
    ]

The `jwks_uri` must use `https`. For local development against a
plain-HTTP authorization server, set `allow_insecure_jwks: true` in the
provider config.

Fetches use conservative HTTP settings: redirects are not followed,
requests time out (5s connect / 10s receive), and responses are capped
at 1MB. If a refresh fails and previously fetched keys are still cached,
those stale keys are served (with a logged warning) so a transient
authorization-server outage does not hard-fail all authentication —
bounded by `:stale_max_age` (default 24 hours), after which the provider
fails closed so revoked keys cannot validate tokens indefinitely.

## Security considerations

**The `jwks_uri` must be trusted operator config — never client-derived.**
Fetches are hardened against SSRF abuse (`https`-only unless
`allow_insecure_jwks`, redirects disabled, 1MB body cap), but the URI
*itself* is not range-checked: a `jwks_uri` pointing at a private or
link-local address — e.g. the cloud metadata endpoint
`http(s)://169.254.169.254/...` — **is fetched, not rejected**. Set
`jwks_uri` from a configuration source you control. If it must come from a
less-trusted source, restrict outbound egress at the network layer (the
library deliberately does not block private ranges, since for most
deployments the JWKS endpoint *is* an internal/private host).

**Revocation lag during an outage.** While the JWKS endpoint is
unreachable, cached keys keep validating tokens until `:stale_max_age`
(default 24h), then the provider fails closed. The trade-off: a key
revoked *during* an outage can still validate tokens for up to
`:stale_max_age`. Lower it if your threat model needs faster revocation;
raise it to tolerate longer authorization-server outages.

## Refresh coordination

Every authenticated request runs `fetch_keys/1`, and `fetch_key/2` calls
`refresh_keys/1` for any `kid` it has not seen. Both are single-flighted
through a lock row in the cache table:

  * **Single flight.** Exactly one process performs an outbound fetch per
    `jwks_uri` at a time. Others wait briefly for the cache to be filled
    and then read it, falling back to the stale-cache path. Without this,
    a TTL lapse at 500 rps produced 500 simultaneous fetches, each holding
    a Bandit process for up to 15 s — and IdPs rate-limit JWKS endpoints,
    so a routine expiry became a total auth outage.

  * **Cooldown.** At most one outbound fetch happens per `jwks_uri` per
    `:refresh_cooldown` (default 30 s), whether or not it succeeded. Inside
    that window an unknown `kid` or a lapsed `:cache_ttl` is answered from
    the cache, still subject to `:stale_max_age`. `fetch_signing_key/2`
    runs *before* any signature check and authentication runs before rate
    limiting, so an unauthenticated caller could otherwise drive one
    outbound fetch per request by inventing a `kid`, or by sending any
    token while the IdP is failing. The window applies to a cold cache
    too: after a failed fetch there is nothing cached to serve, so
    requests inside it get `{:error, :refresh_cooldown}` rather than
    another fetch, and the first request after it fetches again. The
    first fetch for a `jwks_uri` has no cooldown to honour. A request that
    arrives while a fetch is in flight waits for that fetch instead of
    being answered from the cooldown.

  * **Lock age.** A lock older than `:refresh_lock_max_age` (default 30 s,
    comfortably past the HTTP timeouts) is treated as abandoned, so a
    crashed holder cannot wedge refreshes.

## Requirements

Requires the `req` package, `0.6.1` or newer: earlier versions carry
advisories this provider can reach, including unbounded decompression
driven by the response's content-type (GHSA — Req `< 0.6.1`). This
provider additionally sets `compressed: false` and streams the response
through a size-bounded collector, so the 1 MB cap is applied to the bytes
actually received rather than to an already-buffered, already-decompressed
body.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
