Skip to content

Authorization AsyncTokenCredential-returning variants - #568

Merged
Rodrigo Brandão (rodrigobr-msft) merged 15 commits into
mainfrom
users/robrandao/authorization-cred
Sep 10, 2026
Merged

Rodrigo Brandão (rodrigobr-msft) merged 15 commits into
mainfrom
users/robrandao/authorization-cred

Conversation

@rodrigobr-msft

Copy link
Copy Markdown
Contributor

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 AsyncTokenCredential wrappers 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:

  • Added new get_token_credential methods to both SidecarAuth and MsalAuth classes, allowing them to return AsyncTokenCredential objects compatible with Azure SDKs. Corresponding SidecarTokenCredential and MsalTokenCredential classes were implemented to encapsulate the token retrieval logic. [1] [2] [3] [4]
  • Introduced a utility module _helpers.py with the _CallableTokenCredential class, which wraps an async callable to provide an AsyncTokenCredential interface, and helper functions to convert internal TokenResponse objects to Azure's AccessToken.

Authorization API Enhancements:

  • The Authorization class now provides get_token_as_token_credential and exchange_token_as_token_credential methods, enabling consumers to obtain Azure-compatible token credentials directly from authorization flows. [1] [2]
  • Refactored method signatures throughout authorization.py to use | None syntax instead of Optional, improving code clarity and aligning with modern Python typing conventions. [1] [2] [3] [4] [5] [6] [7] [8]

Type Safety and Consistency:

  • Updated various type annotations for handler callbacks and method parameters to use str | None instead of Optional[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:

  • Added necessary imports for AsyncTokenCredential and 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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 AsyncTokenCredential wrappers for MSAL and Sidecar auth providers (MsalTokenCredential, SidecarTokenCredential) and provider entrypoints (get_token_credential).
  • Introduced _CallableTokenCredential + helpers to bridge internal TokenResponse to Azure AccessToken.
  • Extended Authorization with 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 **kwargs through to exchange_token(), but exchange_token() doesn't accept arbitrary kwargs. Azure SDKs commonly pass claims, tenant_id, etc. into get_token(), so this will raise TypeError when used as an AsyncTokenCredential.
        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) when scopes is a non-empty list it short-circuits the or and ignores new_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_id can be None, and :rtype: points to microsoft_agents...authorization.AsyncTokenCredential which isn't a real type; the return type is azure.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.

Copilot AI review requested due to automatic review settings August 31, 2026 18:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings August 31, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_id can be None, and the :rtype: should reference azure.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`

Copilot AI review requested due to automatic review settings August 31, 2026 20:44
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>
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as ready for review August 31, 2026 20:46
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as draft August 31, 2026 20:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings August 31, 2026 20:56
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as ready for review August 31, 2026 20:57
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) marked this pull request as draft August 31, 2026 20:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • overload and Literal are 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

Copilot AI review requested due to automatic review settings August 31, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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() forwards scopes=list(scopes) to exchange_token(). When the Azure SDK (or a consumer) calls credential.get_token() with no scopes, this passes [] rather than None, which prevents exchange_token() from falling back to the handler’s configured default scopes (it only defaults when scopes 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 to exchange_token(). If the wrapper is called with no scopes and the caller didn’t provide initial scopes, this results in [] instead of None, 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,

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 SidecarAuth and SidecarTokenExpiry and the analogous MSAL credential has dedicated tests. Add focused coverage for this factory and wrapper, including scope forwarding, provider reuse, empty scopes, and Azure AccessToken expiry 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 @abstractmethod to the public AccessTokenProviderBase protocol is a runtime-breaking change for existing custom subclasses: providers that implement the previous contract become abstract and raise TypeError when 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>
Copilot AI review requested due to automatic review settings September 10, 2026 15:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 in SidecarTokenCredential and 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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.core from its public authentication implementation, but its own setup.py does not declare azure-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 return is at module scope, so importing this new module raises SyntaxError: 'return' outside function; consequently SidecarAuth.get_token_credential() cannot be used at all. Indent the return statement so it remains inside SidecarTokenCredential.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 to test_msal_token_credential.py for 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_credential as a new abstract member makes existing third-party classes that inherit AccessTokenProviderBase but 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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 to test_msal_token_credential.py so 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.py tests only credentials constructed without a provider. Add a test that calls MsalAuth.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 SidecarAuth that owns an httpx.AsyncClient, but the credential does not override AsyncTokenCredential.close() to close that provider. Consumers using SidecarTokenCredential directly 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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 construct SidecarTokenCredential directly. A test through SidecarAuth.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

  • SidecarTokenCredential is a public, corresponding credential class, but unlike MsalTokenCredential it is not re-exported from microsoft_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_token calls can both observe _provider is None here and construct separate SidecarAuth instances. The later assignment overwrites the first provider, leaving its SidecarHttpClient unclosed; serialize lazy initialization (for example with an asyncio.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 MsalTokenCredential directly and therefore would not catch a broken local import or a credential bound to the wrong provider. Add a test for MsalAuth.get_token_credential() that awaits the returned credential and verifies it delegates through the same MsalAuth instance.
    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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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 older azure-core releases that may not include credentials_async, which would cause runtime import errors.
        "azure-core",
  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) merged commit a078a9c into main Sep 10, 2026
11 checks passed
@rodrigobr-msft
Rodrigo Brandão (rodrigobr-msft) deleted the users/robrandao/authorization-cred branch September 10, 2026 23:39
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.

Expand Authorization class to have TokenCredential-returning variants of existing API

3 participants