infiniflow/ragflow · error · ValueError

Failed to fetch OIDC metadata: {e}

Error message

Failed to fetch OIDC metadata: {e}

What it means

OIDCClient._load_oidc_metadata GETs {issuer}/.well-known/openid-configuration with a 7-second timeout and wraps every failure in ValueError('Failed to fetch OIDC metadata: {e}') (api/apps/auth/oidc.py:110). The nested message distinguishes HTTP errors, timeouts, DNS/connection failures, and JSON parse errors.

Source

Thrown at api/apps/auth/oidc.py:110

        self.jwks_uri = config["jwks_uri"]
        # Pin the accepted ID-token signing algorithms at construction time
        # from a trusted source (provider metadata + safe allowlist) so the
        # JWT verification step in :meth:`parse_id_token` cannot be tricked
        # by attacker-controlled JWT headers (CWE-345 / CWE-347).
        self.id_token_signing_algs = _resolve_id_token_signing_algs(oidc_metadata)

    @staticmethod
    def _load_oidc_metadata(issuer):
        """
        Load OIDC metadata from `/.well-known/openid-configuration`.
        """
        try:
            metadata_url = f"{issuer}/.well-known/openid-configuration"
            response = sync_request("GET", metadata_url, timeout=7)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            raise ValueError(f"Failed to fetch OIDC metadata: {e}")

    def parse_id_token(self, id_token):
        """
        Parse and validate OIDC ID Token (JWT format) with signature verification.

        The accepted signing algorithms come from ``self.id_token_signing_algs``
        (pinned at construction time from the provider's discovery metadata,
        intersected with :data:`_ALLOWED_OIDC_SIGNING_ALGS`). We deliberately
        do **not** read the algorithm from the unverified JWT header — doing
        so would let an attacker bypass signature verification by setting
        ``"alg": "none"`` or pull off the classic RSA / HMAC algorithm
        confusion by setting ``"alg": "HS256"`` and signing with the public
        key fetched from the provider's JWKS (CWE-345 / CWE-347).
        """
        try:
            # Use PyJWT's PyJWKClient to fetch JWKS and find signing key.
            # The client reads the ``kid`` from the JWT header internally to
            # look up the key — that's fine: ``kid`` is not a security

View on GitHub (pinned to 554fb1133a)

Solutions

  1. From the RAGFlow server (inside the container), curl the exact URL: {issuer}/.well-known/openid-configuration - it must return the discovery JSON.
  2. Fix the issuer to the IdP's exact base: for Keycloak that is https://<host>/realms/<realm>; drop any /.well-known/... suffix you may have copied.
  3. Resolve network issues: DNS, egress firewall, proxy env vars, and trust the IdP's CA on the host running RAGFlow.
  4. If the IdP is slow, note the timeout is hardcoded at 7s - reduce IdP latency or serve discovery from a faster endpoint.

Example fix

# verify discovery from inside the container
docker exec ragflow-server curl -sS \
  'https://sso.example.com/realms/main/.well-known/openid-configuration'
# must print JSON containing issuer, jwks_uri, authorization_endpoint, ...
Defensive patterns

Strategy: retry

Validate before calling

import requests

def discovery_reachable(issuer, timeout=7):
    try:
        r = requests.get(f"{issuer}/.well-known/openid-configuration", timeout=timeout)
        return r.ok and isinstance(r.json(), dict) and "issuer" in r.json()
    except Exception:
        return False

# run at provider-config save time and fail fast

Try / catch

for attempt in range(3):
    try:
        client = OIDCClient(config)
        break
    except ValueError as e:
        if "Failed to fetch OIDC metadata" in str(e) and "timed out" in str(e).lower() and attempt < 2:
            continue  # transient timeout - retry
        raise ConfigError(f"OIDC discovery failed: {e}") from e

Prevention

When it happens

Trigger: Issuer URL wrong (typo, wrong port, path with or without trailing slash mismatch vs the IdP's real issuer value); RAGFlow server has no outbound network/DNS to the IdP; TLS certificate invalid; issuer behind a proxy that blocks the well-known path; IdP slow enough to exceed the fixed 7s timeout; response is HTML (auth wall) so .json() raises.

Common situations: Containerized deployments without DNS or egress rules for the IdP host; issuer copied with /auth suffix mismatch (e.g. Keycloak realm URL missing '/realms/<name>'); self-signed certs without a trusted CA; trailing-slash discrepancies producing 404.

Related errors


AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15). Data as JSON: /api/errors/1245d3173487188c. Report an issue: GitHub.