Repository navigation
Authorization AsyncTokenCredential-returning variants - #568
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the Microsoft 365 Agents SDK for Python authentication/authorization surface to interoperate with Azure SDKs by introducing azure.core.credentials_async.AsyncTokenCredential-returning APIs and wrappers, while modernizing type annotations in the OAuth Authorization flow.
Changes:
- Added Azure
AsyncTokenCredentialwrappers for MSAL and Sidecar auth providers (MsalTokenCredential,SidecarTokenCredential) and provider entrypoints (get_token_credential). - Introduced
_CallableTokenCredential+ helpers to bridge internalTokenResponseto AzureAccessToken. - Extended
Authorizationwith credential-returning convenience methods and updated type hints to| None.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| libraries/microsoft-agents-hosting-core/setup.py | Adds Azure dependency for hosting-core (currently uses incorrect distribution name). |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py | Adds get_token_credential() to the provider protocol (new API contract). |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/_helpers.py | Adds TokenResponse → AccessToken conversion and callable-backed AsyncTokenCredential. |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py | Adds credential-returning methods on OAuth Authorization and updates typing. |
| libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/sdk_token_credential.py | Present in PR metadata, but currently empty in the checkout. |
| libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py | Adds get_token_credential() returning an Azure-compatible credential wrapper. |
| libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_token_credential.py | Allows injecting an MSAL provider instance and wraps MSAL token acquisition as AsyncTokenCredential. |
| libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py | Adds get_token_credential() returning an Azure-compatible credential wrapper. |
| libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py | New AsyncTokenCredential wrapper around Sidecar token acquisition. |
Suppressed comments (3)
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:416
get_token_as_token_credential()passes Azure SDK**kwargsthrough toexchange_token(), butexchange_token()doesn't accept arbitrary kwargs. Azure SDKs commonly passclaims,tenant_id, etc. intoget_token(), so this will raiseTypeErrorwhen used as anAsyncTokenCredential.
async def func(*scopes: str, **kwargs) -> TokenResponse:
return await self.exchange_token(
context, auth_handler_id=auth_handler_id, scopes=list(scopes), **kwargs
)
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:506
- In
exchange_token_as_token_credential(),all_scopes = list(set(scopes or [] + list(new_scopes)))has two problems: (1) whenscopesis a non-empty list it short-circuits theorand ignoresnew_scopes, and (2)set()discards order, which can be surprising when scopes are logged/debugged. This can cause credentials to request the wrong scopes.
async def func(*new_scopes: str, **kwargs) -> TokenResponse:
all_scopes = list(set(scopes or [] + list(new_scopes)))
return await self.exchange_token(
context,
scopes=all_scopes,
auth_handler_id=auth_handler_id,
exchange_connection=exchange_connection,
)
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:411
- Docstring types for
get_token_as_token_credential()are inconsistent with the actual signature (e.g.auth_handler_idcan beNone, and:rtype:points tomicrosoft_agents...authorization.AsyncTokenCredentialwhich isn't a real type; the return type isazure.core.credentials_async.AsyncTokenCredential). This can mislead users relying on generated docs.
:param auth_handler_id: The ID of the auth handler to get the token for.
:type auth_handler_id: str
:return: An AsyncTokenCredential for the specified auth handler or the default handler.
:rtype: :class:`microsoft_agents.hosting.core.app.oauth.authorization.AsyncTokenCredential`
"""
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:410
- The docstring types don’t match the signature/return type here:
auth_handler_idcan beNone, and the:rtype:should referenceazure.core.credentials_async.AsyncTokenCredential(the current module path doesn’t exist). This can break generated docs and mislead SDK consumers.
:type context: :class:`microsoft_agents.hosting.core.turn_context.TurnContext`
:param auth_handler_id: The ID of the auth handler to get the token for.
:type auth_handler_id: str
:return: An AsyncTokenCredential for the specified auth handler or the default handler.
:rtype: :class:`microsoft_agents.hosting.core.app.oauth.authorization.AsyncTokenCredential`
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:7
overloadandLiteralare imported but never used in this module, which adds avoidable lint noise and makes the import list harder to scan. Remove the unused names from the typing import.
from typing import Optional, Callable, Awaitable, cast, overload, Literal
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:416
get_token_as_token_credential()forwardsscopes=list(scopes)toexchange_token(). When the Azure SDK (or a consumer) callscredential.get_token()with no scopes, this passes[]rather thanNone, which preventsexchange_token()from falling back to the handler’s configured default scopes (it only defaults whenscopes is None).
This issue also appears on line 500 of the same file.
async def func(*scopes: str, **_kwargs) -> TokenResponse:
return await self.exchange_token(
context, auth_handler_id=auth_handler_id, scopes=list(scopes)
)
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py:504
exchange_token_as_token_credential()always passes a (possibly empty) list of scopes toexchange_token(). If the wrapper is called with no scopes and the caller didn’t provide initialscopes, this results in[]instead ofNone, which prevents the downstream handler from using its configured default scopes.
prev_scopes: list[str] = scopes or []
all_scopes = list(dict.fromkeys([*prev_scopes, *new_scopes]))
return await self.exchange_token(
context,
scopes=all_scopes,
…onse.expiration field set
There was a problem hiding this comment.
🟡 Changes recommended
A critical expiry conversion defect and a moderate backward-compatibility risk remain; the Sidecar credential path also lacks focused tests.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py:116
- The new sidecar credential path has no tests, even though this package already covers
SidecarAuthandSidecarTokenExpiryand the analogous MSAL credential has dedicated tests. Add focused coverage for this factory and wrapper, including scope forwarding, provider reuse, empty scopes, and AzureAccessTokenexpiry conversion.
def get_token_credential(self) -> AsyncTokenCredential:
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py:42
- Adding this
@abstractmethodto the publicAccessTokenProviderBaseprotocol is a runtime-breaking change for existing custom subclasses: providers that implement the previous contract become abstract and raiseTypeErrorwhen instantiated. Since Azure credential support is an optional new capability, consider a separate capability protocol or a backward-compatible default; otherwise document and version this breaking requirement explicitly.
@abstractmethod
def get_token_credential(self) -> AsyncTokenCredential:
"""
Get the token credential for the access token provider.
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Fix the Sidecar credential syntax error and add the missing Sidecar credential test coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py:124
- The new sidecar credential path has no test coverage:
SidecarAuth.get_token_credential()is never exercised, so the syntax error inSidecarTokenCredentialand regressions in scope forwarding/expiry conversion are not caught. Add sidecar credential tests analogous to the existing MSAL credential tests.
def get_token_credential(self) -> AsyncTokenCredential:
"""Gets the token credential for the access token provider.
:return: The token credential.
:rtype: AsyncTokenCredential
"""
from .sidecar_token_credential import SidecarTokenCredential
return SidecarTokenCredential(self._configuration, provider=self)
- Files reviewed: 15/15 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The Sidecar module cannot currently be imported, and compatibility, dependency, and test issues remain unresolved.
Review details
Suppressed comments (4)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py:15
- The sidecar package now imports
azure.corefrom its public authentication implementation, but its ownsetup.pydoes not declareazure-core; it currently relies on the hosting-core dependency to pull it in transitively. Declare the imported runtime dependency directly, as the MSAL package does, so this package's metadata remains correct if that transitive dependency changes.
from azure.core.credentials_async import AsyncTokenCredential
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:69
- The
returnis at module scope, so importing this new module raisesSyntaxError: 'return' outside function; consequentlySidecarAuth.get_token_credential()cannot be used at all. Indent the return statement so it remains insideSidecarTokenCredential.get_token().
return AccessToken(
token=token, expires_on=int(SidecarTokenExpiry.resolve(token).timestamp())
)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:64
- This new credential implementation has no sidecar-credential tests; the existing sidecar tests exercise
SidecarAuth.get_access_token()but never call this wrapper. Add coverage analogous totest_msal_token_credential.pyfor scope validation, provider reuse/forwarding, and the JWT/opaque-token expiration conversion so regressions in this integration path are caught.
async def get_token(self, *scopes: str, **kwargs) -> AccessToken:
"""Acquire an access token for the specified scopes.
:param scopes: The scopes for which the access token is requested.
:param kwargs: Additional keyword arguments.
:return: The acquired access token.
:rtype: AccessToken
"""
logger.debug("get_token scope=%s", scopes)
if not scopes:
raise ValueError("At least one scope must be provided.")
if not self._provider:
self._provider = SidecarAuth(self._config)
resource = _get_resource(scopes[0])
token = await self._provider.get_access_token(resource, list(scopes))
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/access_token_provider_base.py:39
- Adding
get_token_credentialas a new abstract member makes existing third-party classes that inheritAccessTokenProviderBasebut implement only the previous interface non-instantiable at runtime. This is a breaking API change for providers that still use the legacy string-token methods; preserve compatibility with a non-abstract default (or a default adapter) and let providers opt into the Azure credential behavior.
@abstractmethod
def get_token_credential(self) -> AsyncTokenCredential:
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The Sidecar credential lifecycle issue and missing coverage for new credential paths remain unresolved.
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py:124
- The new Sidecar credential path has no tests: the existing sidecar tests cover
SidecarAuth.get_access_token, but not this factory or the returned credential's scope forwarding and expiry conversion. Add tests analogous totest_msal_token_credential.pyso regressions in the Azure credential adapter are caught.
libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py:100 - This new factory path is not covered by the MSAL tests: the existing
test_msal_token_credential.pytests only credentials constructed without a provider. Add a test that callsMsalAuth.get_token_credential()and verifies the returned credential reuses the same provider, since losing that injection would create a separate MSAL client/cache.
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:60
- When this credential is constructed without a provider, the lazy path creates a
SidecarAuththat owns anhttpx.AsyncClient, but the credential does not overrideAsyncTokenCredential.close()to close that provider. Consumers usingSidecarTokenCredentialdirectly can therefore leave the sidecar client open after credential disposal; track provider ownership and forward cleanup (or otherwise define the provider lifetime).
if not self._provider:
self._provider = SidecarAuth(self._config)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:64
- The new SidecarTokenCredential path has no tests, while the analogous MsalTokenCredential has a dedicated suite. In particular, scope/resource forwarding, provider reuse, and conversion of JWT or opaque-token expiry are unverified at this Azure credential boundary; add focused coverage before relying on this new public integration path.
resource = _get_resource(scopes[0])
token = await self._provider.get_access_token(resource, list(scopes))
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Sidecar credential initialization has an unresolved concurrency issue, and requested API coverage and export updates remain outstanding.
Review details
Suppressed comments (4)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_auth.py:124
- The new public factory is not covered by
test_sidecar_auth.py; the credential tests only constructSidecarTokenCredentialdirectly. A test throughSidecarAuth.get_token_credential()should verify that the returned credential delegates to the existing provider, since that binding also controls the documented provider-lifetime behavior.
def get_token_credential(self) -> AsyncTokenCredential:
"""Gets the token credential for the access token provider.
:return: The token credential.
:rtype: AsyncTokenCredential
"""
from .sidecar_token_credential import SidecarTokenCredential
return SidecarTokenCredential(self._configuration, provider=self)
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:30
SidecarTokenCredentialis a public, corresponding credential class, but unlikeMsalTokenCredentialit is not re-exported frommicrosoft_agents.authentication.entra_auth_sidecar.__init__. Consumers cannot import the new type from the package root even though the class is part of this credential API; add the import and include it in__all__.
class SidecarTokenCredential(AsyncTokenCredential):
"""Provides an asynchronous Azure Core token credential using the Sidecar."""
libraries/microsoft-agents-authentication-entra-auth-sidecar/microsoft_agents/authentication/entra_auth_sidecar/sidecar_token_credential.py:63
- Concurrent
get_tokencalls can both observe_provider is Nonehere and construct separateSidecarAuthinstances. The later assignment overwrites the first provider, leaving itsSidecarHttpClientunclosed; serialize lazy initialization (for example with anasyncio.Lock) or initialize the provider eagerly.
if not self._provider:
self._provider = SidecarAuth(self._config)
libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py:100
- The new public factory is not exercised by the MSAL tests: the existing credential tests construct
MsalTokenCredentialdirectly and therefore would not catch a broken local import or a credential bound to the wrong provider. Add a test forMsalAuth.get_token_credential()that awaits the returned credential and verifies it delegates through the sameMsalAuthinstance.
def get_token_credential(self) -> AsyncTokenCredential:
"""Gets the token credential for the access token provider.
:return: The token credential.
:rtype: AsyncTokenCredential
"""
from .msal_token_credential import MsalTokenCredential
return MsalTokenCredential(self._msal_configuration, provider=self)
- Files reviewed: 16/16 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The newly added azure-core dependency should specify a minimum version to reduce the risk of runtime import errors for azure.core.credentials_async.
Review details
Suppressed comments (1)
libraries/microsoft-agents-hosting-core/setup.py:23
- The code now imports
azure.core.credentials_async.AsyncTokenCredential, but the dependency is unpinned ("azure-core"). Adding a minimum version helps prevent installations resolving to olderazure-corereleases that may not includecredentials_async, which would cause runtime import errors.
"azure-core",
- Files reviewed: 17/17 changed files
- Comments generated: 0 new
- Review effort level: Lite
This pull request introduces significant improvements to the authentication and authorization infrastructure, focusing on enhanced interoperability with Azure SDKs and improved type safety. The main changes include the addition of new
AsyncTokenCredentialwrappers for token retrieval, refactoring of method signatures for clarity, and the introduction of utility helpers to bridge internal token types with Azure's credential interfaces.Azure SDK Interoperability and Token Credential Support:
get_token_credentialmethods to bothSidecarAuthandMsalAuthclasses, allowing them to returnAsyncTokenCredentialobjects compatible with Azure SDKs. CorrespondingSidecarTokenCredentialandMsalTokenCredentialclasses were implemented to encapsulate the token retrieval logic. [1] [2] [3] [4]_helpers.pywith the_CallableTokenCredentialclass, which wraps an async callable to provide anAsyncTokenCredentialinterface, and helper functions to convert internalTokenResponseobjects to Azure'sAccessToken.Authorization API Enhancements:
Authorizationclass now providesget_token_as_token_credentialandexchange_token_as_token_credentialmethods, enabling consumers to obtain Azure-compatible token credentials directly from authorization flows. [1] [2]authorization.pyto use| Nonesyntax instead ofOptional, improving code clarity and aligning with modern Python typing conventions. [1] [2] [3] [4] [5] [6] [7] [8]Type Safety and Consistency:
str | Noneinstead ofOptional[str], and similar changes for other types, enhancing type safety and readability. [1] [2] [3] [4] [5] [6] [7] [8]Dependency Imports and Code Organization:
AsyncTokenCredentialand related Azure SDK types in relevant modules to support the new credential features. [1] [2] [3] [4] [5]These updates collectively make the authentication system more robust, extensible, and compatible with Azure's ecosystem, while also improving code maintainability and developer experience.