Skip to content

feat(llm): Entra ID auth for the Azure OpenAI backend - #2389

Open
keith-oak wants to merge 1 commit into
Graphify-Labs:v8from
keith-oak:feat/azure-entra-auth
Open

feat(llm): Entra ID auth for the Azure OpenAI backend#2389
keith-oak wants to merge 1 commit into
Graphify-Labs:v8from
keith-oak:feat/azure-entra-auth

Conversation

@keith-oak

Copy link
Copy Markdown

The azure backend can only authenticate with AZURE_OPENAI_API_KEY. Azure OpenAI resources provisioned with disableLocalAuth: true cannot 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

bedrock already works exactly this way: detect_backend() routes to it on AWS_PROFILE/AWS_REGION alone with no API key, and _call_bedrock picks up whatever the ambient AWS credential chain provides. This PR gives azure the equivalent.

Usage

export AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com/
export AZURE_OPENAI_AUTH_MODE=entra   # 'aad' also accepted
graphify extract ./docs --backend azure

No key. DefaultAzureCredential resolves environment vars → workload identity → managed identity → Azure CLI → Azure PowerShell → azd, so the same code path covers a laptop running az login and a container using a managed identity. Token refresh is handled by get_bearer_token_provider.

Changes

  • _azure_uses_entra() — explicit opt-in, so nothing changes implicitly
  • _azure_client() — passes azure_ad_token_provider instead of api_key
  • the no-key dispatch guard now exempts azure under Entra, as it already does bedrock and claude-cli
  • detect_backend() — endpoint + auth mode counts as a complete credential
  • new [azure] extra pinning azure-identity (the backend previously had no extra of its own and pointed users at bare pip install openai); the import is deferred, so the key path is unaffected when it is absent
  • README env-var table + backends table

Default behaviour is unchanged — without AZURE_OPENAI_AUTH_MODE the backend takes the same key path as before.

Tests

Seven tests in tests/test_llm_backends.py, built on the existing _install_fake_azure_openai stub plus a matching azure.identity stub, 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, no api_key sent on the Entra path, the aad spelling, key path unchanged by default, detection with and without the endpoint, the dispatcher guard, and the missing-azure-identity error message.

Full suite: 3909 passed, 36 skipped. Two failures on this branch are pre-existing on a clean v8 checkout and unrelated (test_ollama_retry_cap.py needs the openai extra; test_labeling.py::test_label_communities_batches_when_over_batch_size asserts a batch completion order).

Possible follow-up

Treating "endpoint set, no key, no auth mode" as implicit Entra would match bedrock even 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.

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.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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).

Comment thread graphify/llm.py
return get_bearer_token_provider(DefaultAzureCredential(), _AZURE_ENTRA_SCOPE)


def _azure_client(api_key: str, endpoint: str):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_azure_client()

6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant