aaif-goose/goose · critical

Failed to fetch JWKS: ${jwksResp.status}

Error message

Failed to fetch JWKS: ${jwksResp.status}

What it means

Thrown by fetchJwks() in oidc-proxy when GET config.jwks_uri returns a non-ok status after the discovery document was fetched successfully. The JWKS document holds the public keys used to verify token signatures, so this failure disables all token validation. The URL comes from the IdP's own discovery response, not from local config.

Source

Thrown at oidc-proxy/src/index.js:156

let jwksCacheTime = 0;
const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour

async function fetchJwks(issuer) {
  const now = Date.now();
  if (jwksCache && now - jwksCacheTime < JWKS_CACHE_TTL_MS) {
    return jwksCache;
  }

  const wellKnownUrl = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
  const configResp = await fetch(wellKnownUrl);
  if (!configResp.ok) {
    throw new Error(`Failed to fetch OIDC config: ${configResp.status}`);
  }
  const config = await configResp.json();

  const jwksResp = await fetch(config.jwks_uri);
  if (!jwksResp.ok) {
    throw new Error(`Failed to fetch JWKS: ${jwksResp.status}`);
  }

  jwksCache = await jwksResp.json();
  jwksCacheTime = now;
  return jwksCache;
}

function base64UrlDecode(str) {
  const padded = str.replace(/-/g, "+").replace(/_/g, "/");
  const binary = atob(padded);
  return Uint8Array.from(binary, (c) => c.charCodeAt(0));
}

function decodeJwtPart(b64url) {
  return JSON.parse(new TextDecoder().decode(base64UrlDecode(b64url)));
}

const ALG_MAP = {

View on GitHub (pinned to 3810898a74)

Solutions

  1. curl the exact jwks_uri printed in the discovery document from the oidc-proxy host to confirm it returns 200 with a keys array.
  2. If the IdP advertises an internal hostname, fix its advertised URL (e.g. Keycloak hostname provider / frontendUrl) or allow egress to that host.
  3. Check TLS: certificate validity and CA trust for the jwks host specifically.
  4. Note the 1-hour JWKS cache: after fixing the IdP, wait for cache expiry or restart the proxy; add retry-with-backoff if the keys endpoint flakes during rotation.

Example fix

// before
const jwksResp = await fetch(config.jwks_uri);
if (!jwksResp.ok) {
  throw new Error(`Failed to fetch JWKS: ${jwksResp.status}`);
}

// after (log which jwks_uri failed and keep the last good cache on transient failure)
const jwksResp = await fetch(config.jwks_uri);
if (!jwksResp.ok) {
  console.error(`JWKS fetch failed: ${config.jwks_uri} -> ${jwksResp.status}`);
  if (jwksCache && jwksResp.status >= 500) return jwksCache; // serve stale on IdP error
  throw new Error(`Failed to fetch JWKS from ${config.jwks_uri}: ${jwksResp.status}`);
}
Defensive patterns

Strategy: retry

Validate before calling

// Verify the advertised jwks_uri resolves before serving traffic
async function assertJwksUriFetchable(config: { jwks_uri: string }): Promise<void> {
  const resp = await fetch(config.jwks_uri);
  if (!resp.ok) throw new Error(`jwks_uri ${config.jwks_uri} not reachable (${resp.status})`);
  const body = (await resp.json()) as { keys?: unknown[] };
  if (!Array.isArray(body.keys) || body.keys.length === 0) {
    throw new Error(`jwks_uri ${config.jwks_uri} returned no keys`);
  }
}

Type guard

function isJwks(value: unknown): value is { keys: JsonWebKey[] } {
  return (
    typeof value === 'object' && value !== null &&
    Array.isArray((value as { keys?: unknown }).keys)
  );
}

Try / catch

try {
  const jwks = await fetchJwks(issuer);
} catch (error) {
  if (/JWKS/.test(String(error))) {
    // Fail closed: reject tokens rather than skip verification
    throw new Error('Token verification unavailable (JWKS fetch failed)', { cause: error });
  }
  throw error;
}

Prevention

When it happens

Trigger: The IdP advertises a jwks_uri that is unreachable from the proxy host: different domain blocked by egress rules, internal Keycloak hostname not resolvable externally, key-rotation endpoint temporarily 5xx, or TLS certificate mismatch on the keys endpoint.

Common situations: Keycloak/OpenIdP exposing an internal hostname in discovery metadata; mTLS or proxy required for the keys endpoint but not for discovery; IdP maintenance window; expired TLS cert on the jwks host while the issuer host is fine.

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/4ff289bc719dee38. Report an issue: GitHub.