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
- curl the exact jwks_uri printed in the discovery document from the oidc-proxy host to confirm it returns 200 with a keys array.
- If the IdP advertises an internal hostname, fix its advertised URL (e.g. Keycloak hostname provider / frontendUrl) or allow egress to that host.
- Check TLS: certificate validity and CA trust for the jwks host specifically.
- 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
- Configure the IdP to advertise an externally reachable jwks_uri (e.g. Keycloak hostname settings).
- Monitor the keys endpoint with a health check; alert before cache expiry windows expire.
- Never skip signature verification when JWKS is unavailable — fail closed.
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
- Failed to fetch OIDC config: ${configResp.status}
- Failed to receive authorization code
- Failed to exchange code: {} - {}
- Failed to exchange code: {} - {}
- Failed to receive authorization code
AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16).
Data as JSON: /api/errors/4ff289bc719dee38.
Report an issue: GitHub.