{"record":{"id":"f4cc1202f3ea72ab","repo":"infiniflow/ragflow","slug":"error-parsing-id-token-e","errorCode":null,"errorMessage":"Error parsing ID Token: {e}","messagePattern":"Error parsing ID Token: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"api/apps/auth/oidc.py","lineNumber":143,"sourceCode":"        try:\n            # Use PyJWT's PyJWKClient to fetch JWKS and find signing key.\n            # The client reads the ``kid`` from the JWT header internally to\n            # look up the key — that's fine: ``kid`` is not a security\n            # decision, the signature still proves which key was used.\n            jwks_cli = jwt.PyJWKClient(self.jwks_uri)\n            signing_key = jwks_cli.get_signing_key_from_jwt(id_token).key\n\n            # Decode and verify signature against the pinned allowlist.\n            decoded_token = jwt.decode(\n                id_token,\n                key=signing_key,\n                algorithms=list(self.id_token_signing_algs),\n                audience=str(self.client_id),\n                issuer=self.issuer,\n            )\n            return decoded_token\n        except Exception as e:\n            raise ValueError(f\"Error parsing ID Token: {e}\")\n\n    def fetch_user_info(self, access_token, id_token=None, **kwargs):\n        \"\"\"\n        Fetch user info.\n        \"\"\"\n        user_info = {}\n        if id_token:\n            user_info = self.parse_id_token(id_token)\n        user_info.update(super().fetch_user_info(access_token).to_dict())\n        return self.normalize_user_info(user_info)\n\n    async def async_fetch_user_info(self, access_token, id_token=None, **kwargs):\n        user_info = {}\n        if id_token:\n            user_info = self.parse_id_token(id_token)\n        user_info.update((await super().async_fetch_user_info(access_token)).to_dict())\n        return self.normalize_user_info(user_info)\n","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/api/apps/auth/oidc.py#L125-L161","documentation":"OIDCClient.parse_id_token verifies the JWT signature via JWKS and python-jwt.decode with an allowlist of algorithms pinned from discovery metadata, plus audience (client_id) and issuer checks. Any failure - key lookup, signature, exp/iat, aud, iss, or alg allowlist - is re-raised as ValueError('Error parsing ID Token: {e}') (api/apps/auth/oidc.py:143).","triggerScenarios":"client_id in RAGFlow differs from the token's aud claim; issuer config differs from the token's iss claim (trailing slash!); expired token (exp passed, e.g. replayed callback or clock skew); JWKS endpoint unreachable or token signed with a key absent from JWKS (IdP key rotation); token alg not in the allowlist intersected with _ALLOWED_OIDC_SIGNING_ALGS; malformed token string.","commonSituations":"Client ID changed in RAGFlow but not at the IdP (or vice versa); Keycloak/other IdP realm URL with inconsistent trailing slash between discovery issuer and configured issuer; IdP signing-key rotation with cached JWKS; server clock drift causing premature expiry.","solutions":["Ensure RAGFlow's client_id exactly equals the IdP client ID (the aud in the token).","Make the configured issuer byte-identical to the iss claim - watch trailing slashes and realm paths.","Paste the ID token into a JWT debugger and check exp/iss/aud against your config and current time; fix clock skew (NTP) if expiry fires early.","If keys rotated, verify the jwks_uri from discovery is reachable and serves the current keys; confirm the IdP's signing alg (e.g. RS256) is in the discovery metadata's supported list."],"exampleFix":"# inspect the token's claims against config\n# decode header.payload (no verification) and compare:\n#   iss  == https://sso.example.com/realms/main  (exact, incl. no trailing slash)\n#   aud  == <your client_id>\n#   exp  > now","handlingStrategy":"try-catch","validationCode":"# no caller-side code can make a provider-issued JWT valid; precheck config invariants\nassert cfg[\"client_id\"] == expected_aud\nassert cfg[\"issuer\"] == expected_iss\nassert abs(time.time() - ntp_now()) < 60  # clock skew guard on the host","typeGuard":null,"tryCatchPattern":"try:\n    claims = client.parse_id_token(id_token)\nexcept ValueError as e:\n    msg = str(e)\n    if \"exp\" in msg.lower() or \"expired\" in msg.lower():\n        restart_authorization()  # stale token - do not retry the same one\n    elif \"aud\" in msg.lower():\n        raise ConfigError(\"client_id does not match token audience\")\n    elif \"iss\" in msg.lower():\n        raise ConfigError(\"issuer config does not match token issuer\")\n    else:\n        raise","preventionTips":["Keep client_id and issuer identical across RAGFlow config and IdP client settings.","Run NTP on servers validating JWTs to avoid premature exp failures.","Never disable signature/audience/issuer verification to 'make it work'.","On IdP signing-key rotation, verify JWKS freshness; this error is often the first symptom."],"tags":["auth","oidc","jwt","security","validation"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}