Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/openai/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def __init__(self, message: str, request: httpx2.Request, *, body: object | None
self.body = body

if is_dict(body):
self.code = cast(Any, construct_type(type_=Optional[str], value=body.get("code")))
raw_code = body.get("code")
self.code = str(raw_code) if raw_code is not None else None
self.param = cast(Any, construct_type(type_=Optional[str], value=body.get("param")))
self.type = cast(Any, construct_type(type_=str, value=body.get("type")))
else:
Expand Down
7 changes: 5 additions & 2 deletions src/openai/auth/_workload.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import math
import time
import threading
from typing import Any, Generic, TypeVar, Callable, TypedDict, cast
Expand Down Expand Up @@ -305,8 +306,10 @@ def _handle_token_response(self, response: httpx2.Response) -> dict[str, Any]:
)

def _validate_expires_in(self, expires_in: object) -> float:
if not isinstance(expires_in, (int, float)):
raise OpenAIError("Token exchange response did not include a valid expires_in")
if isinstance(expires_in, bool):
expires_in = int(expires_in)
if not isinstance(expires_in, (int, float)) or math.isnan(expires_in) or math.isinf(expires_in) or expires_in <= 0:
raise ValueError("Token exchange response did not include a valid expires_in")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve OpenAIError for invalid expirations

When a subject-token exchange returns a missing, nonnumeric, nonpositive, or nonfinite expires_in, this changes the established failure from OpenAIError to ValueError, unlike the other malformed token-response fields handled immediately above. Applications catching SDK authentication errors can therefore unexpectedly receive an uncaught built-in exception; retain OpenAIError while performing the stricter validation and cover synchronous and asynchronous exchange paths.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

return float(expires_in)

def _token_unusable(self) -> bool:
Expand Down
13 changes: 13 additions & 0 deletions src/openai/lib/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ def _has_auth_header(headers: Headers) -> bool:
return _has_header(headers, "Authorization") or _has_header(headers, "api-key")


def _is_jwt(token: str) -> bool:
"""Check if a token looks like a JWT (used for Azure AD tokens)."""
# JWT tokens have three parts separated by dots: header.payload.signature
# The header is base64-encoded and typically starts with "eyJ" for RS256/HS256 tokens
return token.startswith("eyJ") and token.count(".") == 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply JWT detection to Realtime authentication

For Realtime connections using a JWT-like api_key, both _configure_realtime() implementations bypass this helper and unconditionally emit api-key, so the advertised AAD-token compatibility still fails during WebSocket authentication even though ordinary requests attempt Bearer authentication. Apply the same detection when constructing sync and async Realtime headers and cover both entry points.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.



_AZURE_AUTH_ORIGIN = "openai.azure_auth_origin"


Expand Down Expand Up @@ -459,6 +466,9 @@ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: # noqa: A
return {"Authorization": f"Bearer {self._azure_ad_token}"}

if self.api_key and self.api_key != API_KEY_SENTINEL:
# If api_key looks like a JWT (Azure AD token), send as Bearer
if _is_jwt(self.api_key):
return {"Authorization": f"Bearer {self.api_key}"}
Comment on lines +470 to +471

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop re-adding JWTs as API keys

When a JWT-like value is supplied through api_key, _prepare_options() still inserts it into the api-key header before _build_headers() merges in this new Bearer header. Consequently, both synchronous and asynchronous HTTP requests send both authentication headers rather than replacing API-key authentication, so Azure deployments that reject or prioritize the invalid API-key credential can still return 401. Handle _is_jwt() in both _prepare_options() implementations and add sync/async assertions against the final request headers.

AGENTS.md reference: AGENTS.md:L41-L45

Useful? React with 👍 / 👎.

return {"api-key": self.api_key}

return {}
Expand Down Expand Up @@ -813,6 +823,9 @@ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: # noqa: A
return {"Authorization": f"Bearer {self._azure_ad_token}"}

if self.api_key and self.api_key != API_KEY_SENTINEL:
# If api_key looks like a JWT (Azure AD token), send as Bearer
if _is_jwt(self.api_key):
return {"Authorization": f"Bearer {self.api_key}"}
return {"api-key": self.api_key}

return {}
Expand Down