BerriAI/litellm · error · Exception

OIDC UserInfo endpoint returned status {response.status_code

Error message

OIDC UserInfo endpoint returned status {response.status_code}: {response.text}

What it means

Raised in get_userinfo when the OIDC UserInfo endpoint answers with a non-200 status. The response body is included in the message, so the IdP's own error text (e.g. 'invalid token', 'insufficient scope') is visible. Successful responses are cached per-token-hash for oidc_userinfo_cache_ttl, so this only fires on cache misses.

Source

Thrown at litellm/proxy/auth/handle_jwt.py:767

        if cached_userinfo is not None:
            verbose_proxy_logger.debug("Returning cached OIDC UserInfo")
            return cached_userinfo

        verbose_proxy_logger.debug("Calling OIDC UserInfo endpoint: %s", self.litellm_jwtauth.oidc_userinfo_endpoint)

        try:
            # Call the UserInfo endpoint with the access token
            response: Final = await self.http_handler.get(
                url=self.litellm_jwtauth.oidc_userinfo_endpoint,
                headers={
                    "Authorization": f"Bearer {token}",
                    "Accept": "application/json",
                },
            )

            if response.status_code != 200:
                raise Exception(f"OIDC UserInfo endpoint returned status {response.status_code}: {response.text}")

            userinfo: Final = response.json()
            verbose_proxy_logger.debug("Received OIDC UserInfo: %s", userinfo)

            # Cache the userinfo response
            await self.user_api_key_cache.async_set_cache(
                key=cache_key,
                value=userinfo,
                ttl=self.litellm_jwtauth.oidc_userinfo_cache_ttl,
            )

            return userinfo

        except Exception as e:
            verbose_proxy_logger.error("Error fetching OIDC UserInfo: %s", e)
            raise Exception(f"Failed to fetch OIDC UserInfo: {e}")

    _unscoped_jwt_warning_emitted = False

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Reproduce manually: curl -H 'Authorization: Bearer <token>' <userinfo_endpoint> and read the IdP's error body included in the message
  2. If the token is expired or revoked, obtain a fresh access token and retry
  3. Ensure the requested scopes include openid when the token is minted
  4. Confirm oidc_userinfo_endpoint matches the realm/tenant that issued the token
Defensive patterns

Strategy: validation

Validate before calling

import time, jwt as pyjwt

def token_is_fresh(token: str, skew_seconds: int = 30) -> bool:
    payload = pyjwt.decode(token, options={"verify_signature": False})
    exp = payload.get("exp")
    return exp is None or exp - skew_seconds > time.time()

Try / catch

# the IdP's error body is embedded in the message - surface it
t
try:
    await proxy_call_with_jwt(token)
except Exception as e:
    if "OIDC UserInfo endpoint returned status 401" in str(e):
        token = await refresh_access_token()  # expired/revoked at the IdP
    else:
        raise

Prevention

When it happens

Trigger: The proxy calls GET <oidc_userinfo_endpoint> with Authorization: Bearer <access token> and the IdP returns 401 (token expired/revoked or lacking the openid scope), 403 (forbidden), or 5xx - most commonly an expired access token that still passed local JWT verification, or a token without the openid scope.

Common situations: See trigger scenarios.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/24af8f15a7c7d84a. Report an issue: GitHub.