decolua/9router · error

Failed to load OIDC discovery document from ${discoveryUrl}

Error message

Failed to load OIDC discovery document from ${discoveryUrl}

What it means

fetchOidcDiscovery fetches `<issuerUrl>/.well-known/openid-configuration` with cache:no-store and throws this error whenever the response is not ok (non-2xx). The OIDC spec requires the issuer to publish its discovery document at this URL; this library throws early so callers (the /auth/oidc login flow) cannot proceed without endpoints (authorization_endpoint, token_endpoint, jwks_uri).

Source

Thrown at src/lib/auth/oidc.js:69

export async function getOidcRuntimeConfig() {
  const settings = await getSettings();
  if (!["oidc", "both"].includes(settings.authMode) || !isOidcConfigured(settings)) return null;

  const issuerUrl = trimTrailingSlashes(settings.oidcIssuerUrl);
  return {
    issuerUrl,
    clientId: settings.oidcClientId.trim(),
    clientSecret: settings.oidcClientSecret.trim(),
    scopes: normalizeScopes(settings.oidcScopes),
    loginLabel: (settings.oidcLoginLabel || DEFAULT_LOGIN_LABEL).trim() || DEFAULT_LOGIN_LABEL,
  };
}

export async function fetchOidcDiscovery(issuerUrl) {
  const discoveryUrl = `${trimTrailingSlashes(issuerUrl)}/.well-known/openid-configuration`;
  const res = await fetch(discoveryUrl, { cache: "no-store" });
  if (!res.ok) {
    throw new Error(`Failed to load OIDC discovery document from ${discoveryUrl}`);
  }
  return await res.json();
}

export function createPkcePair() {
  const verifier = crypto.randomBytes(32).toString("base64url");
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
  return { verifier, challenge };
}

export function createOidcState() {
  return crypto.randomBytes(16).toString("base64url");
}

export function createOidcNonce() {
  return crypto.randomBytes(16).toString("base64url");
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Open `<issuerUrl>/.well-known/openid-configuration` in a browser/curl from the server hosting 9router; fix the configured oidcIssuerUrl until it returns JSON with 200.
  2. Ensure the URL includes any required path segments (Keycloak: https://host/realms/<realm>; Okta: https://org.okta.com; Auth0: https://tenant.auth0.com).
  3. Confirm the IdP is reachable from the server (firewall, VPN, DNS, TLS certificate validity) — a 401/403 usually means discovery is behind auth, which is not OIDC-compliant; expose it publicly.
  4. If the provider is OAuth2-only without discovery, use an issuer that supports OIDC or manually configure endpoints.

Example fix

// before
const res = await fetch(discoveryUrl, { cache: "no-store" });
if (!res.ok) {
  throw new Error(`Failed to load OIDC discovery document from ${discoveryUrl}`);
}
// after
const res = await fetch(discoveryUrl, { cache: "no-store" });
if (!res.ok) {
  throw new Error(`Failed to load OIDC discovery document from ${discoveryUrl} (HTTP ${res.status}). Check that oidcIssuerUrl is the correct issuer, e.g. https://idp.example.com/realms/main`);
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate the issuer URL before calling fetchOidcDiscovery
async function assertIssuerReachable(issuerUrl) {
  const url = `${issuerUrl.replace(/\/+$/, "")}/.well-known/openid-configuration`;
  const res = await fetch(url, { method: "HEAD" });
  if (!res.ok) throw new Error(`Issuer discovery not reachable (HTTP ${res.status}): ${url}`);
}

Type guard

function isOidcDiscoveryDoc(doc) {
  return !!doc && typeof doc === "object" &&
    typeof doc.authorization_endpoint === "string" &&
    typeof doc.token_endpoint === "string" &&
    typeof doc.jwks_uri === "string";
}

Try / catch

try {
  const doc = await fetchOidcDiscovery(settings.oidcIssuerUrl);
} catch (err) {
  if (err.message.startsWith("Failed to load OIDC discovery document")) {
    // Show admin a hint: verify oidcIssuerUrl (e.g. include /realms/<realm> for Keycloak) and network reachability
  }
  throw err;
}

Prevention

When it happens

Trigger: Any login flow that calls fetchOidcDiscovery(issuerUrl) where the discovery URL returns 404 (issuer URL wrong or missing path segment, e.g. missing realm for Keycloak), 401/403 (endpoint protected), 5xx (IdP outage), or a redirect-to-HTML that fetch resolves with a non-ok status.

Common situations: Misconfigured oidcIssuerUrl in settings (typo, http vs https, missing tenant/realm path like `/realms/master` for Keycloak or `/realms/<tenant>` for Auth0 needs the Auth0 domain not the issuer-with-path style), issuer behind a firewall/VPN unreachable from the 9router server, IdP serving discovery only on the exact issuer URL (trailing-slash or case mismatch), or using an OAuth2-only provider that has no OIDC discovery endpoint.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/3fbfa0386cb06e73. Report an issue: GitHub.