Skip to content

Comments

Python: Unify Azure credential handling across all packages#4087

Closed
eavanvalkenburg wants to merge 1 commit intomicrosoft:mainfrom
eavanvalkenburg:edvan/unified-azure-credential
Closed

Python: Unify Azure credential handling across all packages#4087
eavanvalkenburg wants to merge 1 commit intomicrosoft:mainfrom
eavanvalkenburg:edvan/unified-azure-credential

Conversation

@eavanvalkenburg
Copy link
Member

Summary

Replace ad_token, ad_token_provider, and get_entra_auth_token with a unified credential parameter across all Azure-related Python packages, using azure.identity.get_bearer_token_provider for automatic token caching and refresh.

Motivation

Changes

New type aliases (_entra_id_authentication.py)

  • AzureCredentialTypes = TokenCredential | AsyncTokenCredential — credential objects
  • AzureTokenProvider = Callable[[], str | Awaitable[str]] — callable token providers
  • resolve_credential_to_token_provider() — wraps credentials using azure.identity.get_bearer_token_provider for auto-refresh

Core package

  • AzureOpenAIChatClient, AzureOpenAIResponsesClient, AzureOpenAIAssistantsClient: Accept credential: AzureCredentialTypes | AzureTokenProvider
  • Removed: ad_token, ad_token_provider parameters, get_entra_auth_token/get_entra_auth_token_async helpers

azure-ai package

  • AzureAIClient, AzureAIAgentClient, AzureAIProjectAgentProvider, AzureAIAgentsProvider: Accept credential: AzureCredentialTypes (credential objects only, since the Azure SDK clients handle token refresh internally)

azure-ai-search package

  • AzureAISearchContextProvider: Accept credential: AzureCredentialTypes

purview package

  • PurviewClient, PurviewPolicyMiddleware, PurviewChatPolicyMiddleware: Accept credential: AzureCredentialTypes | AzureTokenProvider

Breaking Changes

  • Removed ad_token and ad_token_provider parameters from all Azure OpenAI clients
  • Removed get_entra_auth_token and get_entra_auth_token_async helper functions
  • Use credential= parameter instead (accepts sync/async credentials and callable token providers)

Testing

All existing tests pass across all affected packages (core, azure-ai, azure-ai-search, purview). All fmt, lint, pyright, and mypy checks pass.

Fixes #3449
Fixes #3500

Replace ad_token, ad_token_provider, and get_entra_auth_token with a
unified credential parameter across all Azure-related packages.

Core changes:
- Add AzureCredentialTypes (TokenCredential | AsyncTokenCredential) and
  AzureTokenProvider (Callable[[], str | Awaitable[str]]) type aliases
- Add resolve_credential_to_token_provider() using azure.identity's
  get_bearer_token_provider for automatic token caching/refresh
- Update AzureOpenAIChatClient, AzureOpenAIResponsesClient, and
  AzureOpenAIAssistantsClient to accept credential: AzureCredentialTypes |
  AzureTokenProvider
- Remove ad_token, ad_token_provider params and get_entra_auth_token helpers

Package updates:
- azure-ai: Accept AzureCredentialTypes on AzureAIClient,
  AzureAIAgentClient, AzureAIProjectAgentProvider, AzureAIAgentsProvider
- azure-ai-search: Accept AzureCredentialTypes on
  AzureAISearchContextProvider
- purview: Accept AzureCredentialTypes | AzureTokenProvider on
  PurviewClient, PurviewPolicyMiddleware, PurviewChatPolicyMiddleware

Fixes microsoft#3449
Fixes microsoft#3500

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 19, 2026 15:39
@markwallace-microsoft
Copy link
Member

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/azure-ai-search/agent_framework_azure_ai_search
   _context_provider.py267398%104–105, 274
packages/azure-ai/agent_framework_azure_ai
   _agent_provider.py110199%250
   _chat_client.py4787584%388–389, 391, 575, 580–581, 583–584, 587, 590, 592, 597, 858–859, 861, 864, 867, 870–875, 878, 880, 888, 900–902, 906, 909–910, 918–921, 931, 939–942, 944–945, 947–948, 955, 963–964, 972–973, 978–979, 983–990, 995, 998, 1006, 1012, 1020–1022, 1025, 1047–1048, 1181, 1209, 1224, 1340, 1462
   _client.py2753786%380, 382, 425, 433–445, 458, 518, 533–538, 581, 616, 618, 651, 859, 862, 865–866, 868–871, 914
   _project_provider.py117496%210, 304, 344, 377
packages/core/agent_framework/azure
   _assistants_client.py380100% 
   _chat_client.py78494%299, 301, 314–315
   _entra_id_authentication.py220100% 
   _responses_client.py49687%185, 217, 281–284
   _shared.py72494%175, 178, 189, 199
packages/purview/agent_framework_purview
   _client.py124298%69–70
   _middleware.py1110100% 
TOTAL21241330484% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
4183 239 💤 0 ❌ 0 🔥 1m 12s ⏱️

@eavanvalkenburg eavanvalkenburg deleted the edvan/unified-azure-credential branch February 19, 2026 15:43
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR standardizes Microsoft Entra ID authentication across the Python Azure integrations by replacing the older ad_token / ad_token_provider / get_entra_auth_token* patterns with a unified credential parameter and a shared credential→token-provider resolver.

Changes:

  • Introduces AzureCredentialTypes, AzureTokenProvider, and resolve_credential_to_token_provider() for consistent token-provider creation and refresh behavior.
  • Updates Azure OpenAI clients (chat/responses/assistants) to accept credential (credential objects or token providers) and always prefer token providers for refresh/caching.
  • Updates Purview middleware/client and Azure AI / Azure AI Search packages to accept the unified credential type and adjusts tests/exports accordingly.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
python/packages/purview/agent_framework_purview/_middleware.py Updates Purview middleware to accept unified credential/token-provider types and refreshes docs.
python/packages/purview/agent_framework_purview/_client.py Expands Purview client auth to accept callable token providers in addition to credential objects.
python/packages/core/tests/azure/test_entra_id_authentication.py Replaces tests for token-fetch helpers with tests for credential→token-provider resolution.
python/packages/core/tests/azure/test_azure_assistants_client.py Updates Assistants client tests to validate new credential resolution behavior and new error messaging.
python/packages/core/agent_framework/azure/_shared.py Centralizes OpenAI Azure client construction around resolving credentials into an azure_ad_token_provider.
python/packages/core/agent_framework/azure/_responses_client.py Updates Responses client to accept unified credential/token-provider types and uses them in both direct and project-client paths.
python/packages/core/agent_framework/azure/_entra_id_authentication.py Adds new type aliases and resolve_credential_to_token_provider() using get_bearer_token_provider.
python/packages/core/agent_framework/azure/_chat_client.py Switches Chat client auth inputs to unified credential approach (credential object or token provider).
python/packages/core/agent_framework/azure/_assistants_client.py Switches Assistants client auth inputs to unified credential approach (credential object or token provider).
python/packages/core/agent_framework/azure/init.pyi Updates public typings to export the new credential/token-provider aliases.
python/packages/core/agent_framework/azure/init.py Adds lazy-export entries for the new credential/token-provider aliases; removes old helper export.
python/packages/azure-ai/agent_framework_azure_ai/_project_provider.py Broadens provider’s credential annotation to the shared alias and updates parameter docs.
python/packages/azure-ai/agent_framework_azure_ai/_client.py Broadens client’s credential annotation to the shared alias and updates parameter docs.
python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py Broadens agent chat client’s credential annotation to the shared alias and updates parameter docs.
python/packages/azure-ai/agent_framework_azure_ai/_agent_provider.py Broadens agent provider’s credential annotation to the shared alias and updates parameter docs.
python/packages/azure-ai-search/agent_framework_azure_ai_search/_context_provider.py Broadens Search context provider’s credential annotation to the shared alias and updates parameter docs.

Comment on lines 43 to 74
@@ -60,10 +62,14 @@ async def close(self) -> None:
await self._client.aclose()

async def _get_token(self, *, tenant_id: str | None = None) -> str:
"""Acquire an access token using either async or sync credential."""
scopes = get_purview_scopes(self._settings)
"""Acquire an access token using either async or sync credential, or callable token provider."""
cred = self._credential
token = cred.get_token(*scopes, tenant_id=tenant_id)
# Callable token provider — returns a token string directly
if callable(cred) and not isinstance(cred, (TokenCredential, AsyncTokenCredential)):
result = cred()
return await result if inspect.isawaitable(result) else result # type: ignore[return-value]
scopes = get_purview_scopes(self._settings)
token = cred.get_token(*scopes, tenant_id=tenant_id) # type: ignore[union-attr]
token = await token if inspect.isawaitable(token) else token
return token.token
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

PurviewClient._get_token() calls sync TokenCredential.get_token() directly on the event loop. This contradicts the class docstring (“invoked in a thread”) and can block the event loop during token acquisition. Consider detecting TokenCredential and running get_token in a worker thread (or switch to always using a bearer token provider) to keep token acquisition non-blocking.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +136
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The docstring says credential accepts “TokenCredential, AsyncTokenCredential, or a callable token provider”, but the type is AzureCredentialTypes | None (no token provider). Either update the docstring to match (credential objects only) or expand the accepted types and implement a safe adapter for callables.

Suggested change
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
credential: Azure credential for authentication. Accepts an AzureCredentialTypes
instance, such as a TokenCredential or AsyncTokenCredential.

Copilot uses AI. Check for mistakes.
resolved_credential: AzureKeyCredential | AsyncTokenCredential
if credential:
resolved_credential = credential
resolved_credential = credential # type: ignore[assignment]
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

AzureAISearchContextProvider now types credential as AzureCredentialTypes (includes sync TokenCredential) and the docstring also mentions callable token providers, but the implementation constructs azure.search.documents.aio.SearchClient and annotates resolved_credential as AzureKeyCredential | AsyncTokenCredential. This is internally inconsistent (note the # type: ignore[assignment]) and may allow passing unsupported credential types to the async SearchClient. Recommend (a) removing the “callable token provider” claim, and (b) narrowing credential back to the credential type(s) that the async SearchClient actually supports, so the type: ignore and mismatched annotations can be removed.

Suggested change
resolved_credential = credential # type: ignore[assignment]
if isinstance(credential, (AzureKeyCredential, AsyncTokenCredential)):
resolved_credential = credential
else:
raise ServiceInitializationError(
"Unsupported credential type for Azure AI Search async client. "
"Use AzureKeyCredential or an AsyncTokenCredential."
)

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +110
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The docstring states credential can be a “callable token provider”, but the parameter type is AzureCredentialTypes | None (credentials only). Please update the docstring to reflect the actual supported types, or expand the accepted types and add explicit support (right now the callable case isn’t handled).

Suggested change
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
credential: Azure credential for authentication. Accepts types compatible with
AzureCredentialTypes (for example, a TokenCredential or AsyncTokenCredential).

Copilot uses AI. Check for mistakes.
Comment on lines +442 to +443
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The docstring claims credential accepts a “callable token provider”, but the parameter type is AzureCredentialTypes | None. This mismatch is user-facing and can lead to runtime errors if callers pass a callable. Either remove the callable claim or update the signature and implement a supported adapter type.

Suggested change
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
credential: Azure credential for authentication. Accepts a TokenCredential
or AsyncTokenCredential.

Copilot uses AI. Check for mistakes.
Comment on lines 241 to 276
def _create_client_from_project(
*,
project_client: AIProjectClient | None,
project_endpoint: str | None,
credential: TokenCredential | None,
credential: AzureCredentialTypes | AzureTokenProvider | None,
) -> AsyncOpenAI:
"""Create an AsyncOpenAI client from an Azure AI Foundry project.

Args:
project_client: An existing AIProjectClient to use.
project_endpoint: The Azure AI Foundry project endpoint URL.
credential: Azure credential for authentication.

Returns:
An AsyncAzureOpenAI client obtained from the project client.

Raises:
ServiceInitializationError: If required parameters are missing or
the azure-ai-projects package is not installed.
"""
if project_client is not None:
return project_client.get_openai_client()

if not project_endpoint:
raise ServiceInitializationError(
"Azure AI project endpoint is required when project_client is not provided."
)
if not credential:
raise ServiceInitializationError(
"Azure credential is required when using project_endpoint without a project_client."
)
project_client = AIProjectClient(
endpoint=project_endpoint,
credential=credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

AzureOpenAIResponsesClient._create_client_from_project() accepts credential: AzureCredentialTypes | AzureTokenProvider, but then passes it to azure.ai.projects.aio.AIProjectClient with # type: ignore[arg-type]. Token providers (and possibly sync TokenCredential) are unlikely to be valid inputs for the async AIProjectClient, so this path can break at runtime. Recommend validating that credential is an Azure credential object supported by AIProjectClient when using project_endpoint, and raising a clear initialization error otherwise (or narrowing the type for this path).

Copilot uses AI. Check for mistakes.
Comment on lines 196 to 203
# Use provided credential
if not credential:
raise ServiceInitializationError("Azure credential is required when project_client is not provided.")
project_client = AIProjectClient(
endpoint=resolved_endpoint,
credential=credential,
credential=credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

credential is now typed as AzureCredentialTypes (includes sync TokenCredential), but it’s passed into the async AIProjectClient constructor with # type: ignore[arg-type]. This suggests the downstream SDK expects a narrower credential type (likely AsyncTokenCredential). To avoid runtime auth failures and to keep typing honest, either restrict this parameter back to the supported async credential type or add explicit runtime handling/conversion instead of ignoring the type mismatch.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +120
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The docstring says credential accepts “TokenCredential, AsyncTokenCredential, or a callable token provider”, but the parameter is typed as AzureCredentialTypes | None (no token provider). Please align the docs with the actual supported types to avoid misleading users.

Suggested change
credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider.
credential: Azure credential for authentication. Accepts credentials supported by
AzureCredentialTypes (for example, TokenCredential or AsyncTokenCredential).

Copilot uses AI. Check for mistakes.
Comment on lines 148 to 155
if not credential:
raise ServiceInitializationError("Azure credential is required when project_client is not provided.")

project_client = AIProjectClient(
endpoint=resolved_endpoint,
credential=credential,
credential=credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

credential is now AzureCredentialTypes (includes sync TokenCredential) but is passed into azure.ai.projects.aio.AIProjectClient with # type: ignore[arg-type]. If the SDK only supports AsyncTokenCredential for its async client, accepting TokenCredential here will be a breaking runtime bug. Recommend narrowing the type to the actually supported credential type(s) or adding explicit handling instead of suppressing the type checker.

Copilot uses AI. Check for mistakes.
Comment on lines 137 to 143
if not credential:
raise ServiceInitializationError("Azure credential is required when agents_client is not provided.")
self._agents_client = AgentsClient(
endpoint=resolved_endpoint,
credential=credential,
credential=credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT,
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

credential is typed as AzureCredentialTypes (includes sync TokenCredential) but is passed to the async AgentsClient constructor with # type: ignore[arg-type]. To avoid accepting unsupported credential types, narrow credential to what AgentsClient actually supports (likely AsyncTokenCredential) or add explicit handling instead of suppressing the type error.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants