Skip to content

Commit 670aa2d

Browse files
committed
[Identity] Enable brokered auth in DAC
Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
1 parent 81df908 commit 670aa2d

File tree

5 files changed

+154
-2
lines changed

5 files changed

+154
-2
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- Valid values are `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `InteractiveBrowserCredential`. ([#41709](https://github.com/Azure/azure-sdk-for-python/pull/41709))
99
- Re-enabled `VisualStudioCodeCredential` - Previously deprecated `VisualStudioCodeCredential` has been re-implemented to work with the VS Code Azure Resources extension instead of the deprecated Azure Account extension. This requires the `azure-identity-broker` package to be installed for authentication. ([#41822](https://github.com/Azure/azure-sdk-for-python/pull/41822))
1010
- `VisualStudioCodeCredential` is now included in the `DefaultAzureCredential` token chain by default.
11-
11+
- `DefaultAzureCredential` now supports authentication with the currently signed-in Windows account, provided the `azure-identity-broker` package is installed. This auth mechanism is added at the end of the `DefaultAzureCredential` credential chain. ([#40335](https://github.com/Azure/azure-sdk-for-python/pull/40335))
1212

1313
### Breaking Changes
1414

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# ------------------------------------
2+
# Copyright (c) Microsoft Corporation.
3+
# Licensed under the MIT License.
4+
# ------------------------------------
5+
import sys
6+
from typing import Any
7+
8+
import msal
9+
from azure.core.credentials import AccessToken, AccessTokenInfo, SupportsTokenInfo
10+
from .._exceptions import CredentialUnavailableError
11+
from .._internal.utils import get_broker_credential, is_wsl
12+
13+
14+
class BrokerCredential(SupportsTokenInfo):
15+
"""A broker credential that handles prerequisite checking and falls back appropriately.
16+
17+
This credential checks if the azure-identity-broker package is available and the platform
18+
is supported. If both conditions are met, it uses the real broker credential. Otherwise,
19+
it raises CredentialUnavailableError with an appropriate message.
20+
"""
21+
22+
def __init__(self, **kwargs: Any) -> None:
23+
24+
self._tenant_id = kwargs.pop("tenant_id", None)
25+
self._client_id = kwargs.pop("client_id", None)
26+
self._broker_credential = None
27+
self._unavailable_message = None
28+
29+
# Check prerequisites and initialize the appropriate credential
30+
broker_credential_class = get_broker_credential()
31+
if broker_credential_class and (sys.platform.startswith("win") or is_wsl()):
32+
# The silent auth flow for brokered auth is available on Windows/WSL with the broker package
33+
try:
34+
broker_credential_args = {
35+
"tenant_id": self._tenant_id,
36+
"parent_window_handle": msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE,
37+
"use_default_broker_account": True,
38+
**kwargs,
39+
}
40+
if self._client_id:
41+
broker_credential_args["client_id"] = self._client_id
42+
self._broker_credential = broker_credential_class(**broker_credential_args)
43+
except Exception as ex: # pylint: disable=broad-except
44+
self._unavailable_message = f"InteractiveBrowserBrokerCredential initialization failed: {ex}"
45+
else:
46+
# Determine the specific reason for unavailability
47+
if broker_credential_class is None:
48+
self._unavailable_message = (
49+
"InteractiveBrowserBrokerCredential unavailable. "
50+
"The 'azure-identity-broker' package is required to use brokered authentication."
51+
)
52+
else:
53+
self._unavailable_message = (
54+
"InteractiveBrowserBrokerCredential unavailable. "
55+
"Brokered authentication is only supported on Windows and WSL platforms."
56+
)
57+
58+
def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
59+
if self._broker_credential:
60+
return self._broker_credential.get_token(*scopes, **kwargs)
61+
raise CredentialUnavailableError(message=self._unavailable_message)
62+
63+
def get_token_info(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
64+
if self._broker_credential:
65+
return self._broker_credential.get_token_info(*scopes, **kwargs)
66+
raise CredentialUnavailableError(message=self._unavailable_message)
67+
68+
def __enter__(self) -> "BrokerCredential":
69+
if self._broker_credential:
70+
self._broker_credential.__enter__()
71+
return self
72+
73+
def __exit__(self, *args):
74+
if self._broker_credential:
75+
self._broker_credential.__exit__(*args)
76+
77+
def close(self) -> None:
78+
self.__exit__()

sdk/identity/azure-identity/azure/identity/_credentials/default.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, SupportsTokenInfo, TokenCredential
1010
from .._constants import EnvironmentVariables
11-
from .._internal import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
11+
from .._internal.utils import get_default_authority, normalize_authority, within_dac, process_credential_exclusions
1212
from .azure_powershell import AzurePowerShellCredential
13+
from .broker import BrokerCredential
1314
from .browser import InteractiveBrowserCredential
1415
from .chained import ChainedTokenCredential
1516
from .environment import EnvironmentCredential
@@ -42,6 +43,9 @@ class DefaultAzureCredential(ChainedTokenCredential):
4243
5. The identity currently logged in to the Azure CLI.
4344
6. The identity currently logged in to Azure PowerShell.
4445
7. The identity currently logged in to the Azure Developer CLI.
46+
8. Brokered authentication. On Windows and WSL, this uses an authentication broker such as
47+
Web Account Manager (WAM). On other platforms or when the azure-identity-broker package is not installed,
48+
this credential will raise CredentialUnavailableError.
4549
4650
This default behavior is configurable with keyword arguments.
4751
@@ -64,6 +68,9 @@ class DefaultAzureCredential(ChainedTokenCredential):
6468
**False**.
6569
:keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see
6670
:class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**.
71+
:keyword bool exclude_broker_credential: Whether to exclude the broker credential from the credential chain.
72+
Defaults to **False**. When False, the broker credential is always included in the chain but will raise
73+
CredentialUnavailableError if the azure-identity-broker package is not installed.
6774
:keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through
6875
:class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable
6976
AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
@@ -147,6 +154,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
147154
},
148155
"visual_studio_code": {
149156
"exclude_param": "exclude_visual_studio_code_credential",
157+
"env_name": "visualstudiocodecredential",
150158
"default_exclude": False,
151159
},
152160
"cli": {
@@ -169,6 +177,11 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
169177
"env_name": "interactivebrowsercredential",
170178
"default_exclude": True,
171179
},
180+
"broker": {
181+
"exclude_param": "exclude_broker_credential",
182+
"env_name": "interactivebrowserbrokercredential",
183+
"default_exclude": False,
184+
},
172185
}
173186

174187
# Extract user-provided exclude flags and set defaults
@@ -192,6 +205,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
192205
exclude_developer_cli_credential = exclude_flags["developer_cli"]
193206
exclude_powershell_credential = exclude_flags["powershell"]
194207
exclude_interactive_browser_credential = exclude_flags["interactive_browser"]
208+
exclude_broker_credential = exclude_flags["broker"]
195209

196210
credentials: List[SupportsTokenInfo] = []
197211
within_dac.set(True)
@@ -242,6 +256,12 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
242256
)
243257
else:
244258
credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs))
259+
if not exclude_broker_credential:
260+
broker_credential_args = {"tenant_id": interactive_browser_tenant_id, **kwargs}
261+
if interactive_browser_client_id:
262+
broker_credential_args["client_id"] = interactive_browser_client_id
263+
credentials.append(BrokerCredential(**broker_credential_args))
264+
245265
within_dac.set(False)
246266
super(DefaultAzureCredential, self).__init__(*credentials)
247267

sdk/identity/azure-identity/azure/identity/_internal/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import os
6+
import platform
67
import logging
78
from contextvars import ContextVar
89
from string import ascii_letters, digits
@@ -209,3 +210,11 @@ def get_broker_credential() -> Optional[type]:
209210
return InteractiveBrowserBrokerCredential
210211
except ImportError:
211212
return None
213+
214+
215+
def is_wsl() -> bool:
216+
# This is how MSAL checks for WSL.
217+
uname = platform.uname()
218+
platform_name = getattr(uname, "system", uname[0]).lower()
219+
release = getattr(uname, "release", uname[2]).lower()
220+
return platform_name == "linux" and "microsoft" in release

sdk/identity/azure-identity/tests/test_default.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
import os
6+
import sys
67

78
from azure.core.credentials import AccessToken, AccessTokenInfo
89
from azure.identity import (
@@ -19,6 +20,7 @@
1920
from azure.identity._credentials.azure_cli import AzureCliCredential
2021
from azure.identity._credentials.azd_cli import AzureDeveloperCliCredential
2122
from azure.identity._credentials.managed_identity import ManagedIdentityCredential
23+
from azure.identity._internal.utils import is_wsl
2224
import pytest
2325
from urllib.parse import urlparse
2426

@@ -175,12 +177,25 @@ def assert_credentials_not_present(chain, *excluded_credential_classes):
175177
credential = DefaultAzureCredential(exclude_developer_cli_credential=True)
176178
assert_credentials_not_present(credential, AzureDeveloperCliCredential)
177179

180+
# test excluding broker credential
181+
credential = DefaultAzureCredential(exclude_broker_credential=True)
182+
from azure.identity._credentials.broker import BrokerCredential
183+
184+
assert_credentials_not_present(credential, BrokerCredential)
185+
178186
# interactive auth is excluded by default
179187
credential = DefaultAzureCredential(exclude_interactive_browser_credential=False)
180188
actual = {c.__class__ for c in credential.credentials}
181189
default = {c.__class__ for c in DefaultAzureCredential().credentials}
182190
assert actual - default == {InteractiveBrowserCredential}
183191

192+
# broker credential is included by default
193+
credential = DefaultAzureCredential()
194+
from azure.identity._credentials.broker import BrokerCredential
195+
196+
actual = {c.__class__ for c in credential.credentials}
197+
assert BrokerCredential in actual, "BrokerCredential should be included in DefaultAzureCredential by default"
198+
184199

185200
@pytest.mark.parametrize("get_token_method", GET_TOKEN_METHODS)
186201
def test_shared_cache_tenant_id(get_token_method):
@@ -432,3 +447,33 @@ def test_validate_cloud_shell_credential_in_dac():
432447
DefaultAzureCredential(identity_config={"client_id": "foo"})
433448
DefaultAzureCredential(identity_config={"object_id": "foo"})
434449
DefaultAzureCredential(identity_config={"resource_id": "foo"})
450+
451+
452+
@pytest.mark.skipif(not sys.platform.startswith("win") and not is_wsl(), reason="tests Windows-specific behavior")
453+
def test_broker_credential():
454+
"""Test that DefaultAzureCredential uses the broker credential when available"""
455+
with patch("azure.identity.broker.InteractiveBrowserBrokerCredential") as mock_credential:
456+
credential = DefaultAzureCredential()
457+
# The broker credential should be in the chain
458+
broker_credentials = [c for c in credential.credentials if c.__class__.__name__ == "BrokerCredential"]
459+
assert len(broker_credentials) == 1, "BrokerCredential should be in the chain"
460+
# InteractiveBrowserBrokerCredential should be instantiated by BrokerCredential
461+
assert mock_credential.call_count > 1, "InteractiveBrowserBrokerCredential should be instantiated"
462+
463+
464+
def test_broker_credential_requirements_not_installed():
465+
"""Test that DefaultAzureCredential includes BrokerCredential even when broker package is not installed"""
466+
467+
# Mock the get_broker_credential function to return None (simulating package not installed)
468+
with patch.dict("sys.modules", {"azure.identity.broker": None}):
469+
credential = DefaultAzureCredential()
470+
# The broker credential should still be in the chain
471+
broker_credentials = [c for c in credential.credentials if c.__class__.__name__ == "BrokerCredential"]
472+
assert (
473+
len(broker_credentials) == 1
474+
), "BrokerCredential should be in the chain even when broker package is not installed"
475+
476+
# Test that the broker credential raises CredentialUnavailableError
477+
broker_cred = broker_credentials[0]
478+
with pytest.raises(CredentialUnavailableError) as exc_info:
479+
broker_cred.get_token_info("https://management.azure.com/.default")

0 commit comments

Comments
 (0)