feat(llm): Entra ID auth for the Azure OpenAI backend - #2389
Conversation
The azure backend could only authenticate with AZURE_OPENAI_API_KEY. Azure OpenAI resources provisioned with disableLocalAuth: true cannot issue an API key at all, so those resources were unusable — and key-based auth is the posture Azure recommends against generally. Set AZURE_OPENAI_AUTH_MODE=entra (or the older spelling, aad) alongside AZURE_OPENAI_ENDPOINT to authenticate via DefaultAzureCredential instead. That resolves environment vars, workload identity, managed identity, the Azure CLI and Azure PowerShell in the standard order, so one code path covers a laptop running az login and a container using managed identity. This mirrors how bedrock already works: it takes no API key and authenticates from the ambient AWS credential chain. - _azure_uses_entra() gates the behaviour on an explicit opt-in - _azure_client() passes azure_ad_token_provider instead of api_key - the no-key dispatch guard exempts azure under Entra, as it does bedrock - detect_backend() accepts endpoint + auth mode as a complete credential - new [azure] extra pins azure-identity; the import stays deferred so the key-based path is unaffected when it is absent Default behaviour is unchanged: without AZURE_OPENAI_AUTH_MODE the backend takes the same key path as before.
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR adds Entra ID (Azure AD) authentication as an alternative to API-key auth for the Azure OpenAI backend in graphify/llm.py. It introduces an AZURE_OPENAI_AUTH_MODE=entra (or aad) option that routes the AzureOpenAI client through a DefaultAzureCredential-based bearer token provider instead of an API key, and updates the key-requirement check and detect_backend to treat Azure as ambiently authenticated in that mode. Supporting changes update the azure extra to include azure-identity, add it to the all extra, extend README docs, and add tests covering the Entra path, credential wiring, and scope.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 683 functions depend on the 289 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract_files_direct()— 16 callers, 20 callees - worse:
detect_backend()— 18 callers, 4 callees - worse:
_azure_client()— 6 callers, 3 callees
Verification — 683 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 471 function(s) in the blast radius were not formally verified this run
· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).
| return get_bearer_token_provider(DefaultAzureCredential(), _AZURE_ENTRA_SCOPE) | ||
|
|
||
|
|
||
| def _azure_client(api_key: str, endpoint: str): |
There was a problem hiding this comment.
_azure_client()
6 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
The
azurebackend can only authenticate withAZURE_OPENAI_API_KEY. Azure OpenAI resources provisioned withdisableLocalAuth: truecannot issue an API key at all, so graphify currently has no way to talk to them — and key-based auth is the posture Azure recommends against generally.Precedent
bedrockalready works exactly this way:detect_backend()routes to it onAWS_PROFILE/AWS_REGIONalone with no API key, and_call_bedrockpicks up whatever the ambient AWS credential chain provides. This PR givesazurethe equivalent.Usage
No key.
DefaultAzureCredentialresolves environment vars → workload identity → managed identity → Azure CLI → Azure PowerShell → azd, so the same code path covers a laptop runningaz loginand a container using a managed identity. Token refresh is handled byget_bearer_token_provider.Changes
_azure_uses_entra()— explicit opt-in, so nothing changes implicitly_azure_client()— passesazure_ad_token_providerinstead ofapi_keyazureunder Entra, as it already doesbedrockandclaude-clidetect_backend()— endpoint + auth mode counts as a complete credential[azure]extra pinningazure-identity(the backend previously had no extra of its own and pointed users at barepip install openai); the import is deferred, so the key path is unaffected when it is absentDefault behaviour is unchanged — without
AZURE_OPENAI_AUTH_MODEthe backend takes the same key path as before.Tests
Seven tests in
tests/test_llm_backends.py, built on the existing_install_fake_azure_openaistub plus a matchingazure.identitystub, so nothing needs the real SDKs. Five fail without this change; the other two are regression guards for the key path. They cover: token provider wired with the correct scope, noapi_keysent on the Entra path, theaadspelling, key path unchanged by default, detection with and without the endpoint, the dispatcher guard, and the missing-azure-identityerror message.Full suite: 3909 passed, 36 skipped. Two failures on this branch are pre-existing on a clean
v8checkout and unrelated (test_ollama_retry_cap.pyneeds theopenaiextra;test_labeling.py::test_label_communities_batches_when_over_batch_sizeasserts a batch completion order).Possible follow-up
Treating "endpoint set, no key, no auth mode" as implicit Entra would match
bedrockeven more closely, but it converts today's clear "No API key" error into a connection attempt, so I left it explicit. Happy to add it if you'd prefer.