BerriAI/litellm · error · AzureOpenAIError

Azure AD Token access_token not returned

Error message

Azure AD Token access_token not returned

What it means

The token endpoint returned HTTP 200 but the JSON body had no access_token field. LiteLLM treats this as a 422 protocol violation because a successful client-credentials response must contain access_token per the OAuth 2.0 spec. It almost always means a non-Azure endpoint or a proxy answered 200 with a different payload.

Source

Thrown at litellm/llms/azure/common_utils.py:245

            "grant_type": "client_credentials",
            "scope": scope,
            "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
            "client_assertion": oidc_token,
        },
    )

    if req_token.status_code != 200:
        raise AzureOpenAIError(
            status_code=req_token.status_code,
            message=req_token.text,
        )

    azure_ad_token_json: Final[_AzureAdTokenJson] = req_token.json()
    azure_ad_token_access_token = azure_ad_token_json.get("access_token", None)
    azure_ad_token_expires_in: Final = azure_ad_token_json.get("expires_in", None)

    if azure_ad_token_access_token is None:
        raise AzureOpenAIError(status_code=422, message="Azure AD Token access_token not returned")

    if azure_ad_token_expires_in is None:
        raise AzureOpenAIError(status_code=422, message="Azure AD Token expires_in not returned")

    azure_ad_cache.set_cache(
        key=azure_ad_token_cache_key,
        value=azure_ad_token_access_token,
        ttl=azure_ad_token_expires_in,
    )

    return azure_ad_token_access_token


def select_azure_base_url_or_endpoint(azure_client_params: dict):
    azure_endpoint: Final = azure_client_params.get("azure_endpoint", None)
    if azure_endpoint is not None:
        # see : https://github.com/openai/openai-python/blob/3d61ed42aba652b547029095a7eb269ad4e1e957/src/openai/lib/azure.py#L192
        if "/openai/deployments" in azure_endpoint:

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Print/inspect the actual response your environment gets: curl -sv $AZURE_AUTHORITY_HOST/$AZURE_TENANT_ID/oauth2/v2.0/token to see what is answering.
  2. Fix AZURE_AUTHORITY_HOST to the real authority (default https://login.microsoftonline.com or the sovereign equivalent).
  3. Bypass corporate proxies/SSL inspection for login.microsoftonline.com (add to NO_PROXY or proxy allowlist).
  4. In tests, make the mocked endpoint return both access_token and expires_in.

Example fix

# before (mock returns incomplete body)
{ "token_type": "Bearer", "expires_in": 3599 }

# after
{ "token_type": "Bearer", "expires_in": 3599, "access_token": "eyJ..." }
Defensive patterns

Strategy: validation

Validate before calling

import os

def sane_authority() -> None:
    host = os.getenv("AZURE_AUTHORITY_HOST", "https://login.microsoftonline.com")
    if "login.microsoftonline" not in host and "login.microsoftonline.us" not in host and "login.chinacloudapi" not in host:
        raise ConfigError(f"Suspicious AZURE_AUTHORITY_HOST: {host}")

Try / catch

try:
    resp = litellm.completion(..., azure_ad_token=oidc)
except AzureOpenAIError as e:
    if e.status_code == 422 and "access_token not returned" in str(e):
        log.error("token endpoint answered 200 without access_token — check AZURE_AUTHORITY_HOST/proxy")
    raise

Prevention

When it happens

Trigger: AZURE_AUTHORITY_HOST pointing at a service that returns 200 with HTML/JSON lacking access_token (a corporate proxy's captive portal, a misconfigured gateway); an API-management wrapper in front of the authority; a mocked test server returning an incomplete fixture.

Common situations: SSL-inspecting proxies rewriting the token response; typos in AZURE_AUTHORITY_HOST that hit a benign web server; test fixtures with only expires_in; adal/MSAL middleware in the path.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/23475fc5e4c7034e. Report an issue: GitHub.