Skip to content

Commit aa29c33

Browse files
authored
[Identity] Enable brokered auth in DAC (#40335)
Brokered auth with WAM is now last in the chain for DAC. Signed-off-by: Paul Van Eck <paulvaneck@microsoft.com>
1 parent e81b4c3 commit aa29c33

File tree

7 files changed

+208
-20
lines changed

7 files changed

+208
-20
lines changed

sdk/identity/azure-identity-broker/azure/identity/broker/_browser.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ class InteractiveBrowserBrokerCredential(_InteractiveBrowserCredential):
5858
are required to also provide its window handle, so that the sign in UI window will properly pop up on top
5959
of your window.
6060
:keyword bool use_default_broker_account: Enables automatically using the default broker account for
61-
authentication instead of prompting the user with an account picker. Defaults to False.
61+
authentication instead of prompting the user with an account picker. This is currently only supported on Windows
62+
and WSL. Defaults to False.
6263
:keyword bool enable_msa_passthrough: Determines whether Microsoft Account (MSA) passthrough is enabled. Note, this
6364
is only needed for select legacy first-party applications. Defaults to False.
6465
:keyword bool disable_instance_discovery: Determines whether or not instance discovery is performed when attempting
@@ -78,6 +79,7 @@ def __init__(self, **kwargs: Any) -> None:
7879
self._parent_window_handle = kwargs.pop("parent_window_handle", None)
7980
self._enable_msa_passthrough = kwargs.pop("enable_msa_passthrough", False)
8081
self._use_default_broker_account = kwargs.pop("use_default_broker_account", False)
82+
self._disable_interactive_fallback = kwargs.pop("disable_interactive_fallback", False)
8183
super().__init__(**kwargs)
8284

8385
@wrap_exceptions
@@ -93,6 +95,7 @@ def _request_token(self, *scopes: str, **kwargs: Any) -> Dict:
9395
http_method=pop["resource_request_method"], url=pop["resource_request_url"], nonce=pop["nonce"]
9496
)
9597
if sys.platform.startswith("win") or is_wsl():
98+
result = {}
9699
if self._use_default_broker_account:
97100
try:
98101
result = app.acquire_token_interactive(
@@ -110,6 +113,10 @@ def _request_token(self, *scopes: str, **kwargs: Any) -> Dict:
110113
return result
111114
except socket.error:
112115
pass
116+
117+
if self._disable_interactive_fallback:
118+
self._check_result(result)
119+
113120
try:
114121
result = app.acquire_token_interactive(
115122
scopes=scopes_list,
@@ -124,14 +131,8 @@ def _request_token(self, *scopes: str, **kwargs: Any) -> Dict:
124131
)
125132
except socket.error as ex:
126133
raise CredentialUnavailableError(message="Couldn't start an HTTP server.") from ex
127-
if "access_token" not in result and "error_description" in result:
128-
if within_dac.get():
129-
raise CredentialUnavailableError(message=result["error_description"])
130-
raise ClientAuthenticationError(message=result.get("error_description"))
131-
if "access_token" not in result:
132-
if within_dac.get():
133-
raise CredentialUnavailableError(message="Failed to authenticate user")
134-
raise ClientAuthenticationError(message="Failed to authenticate user")
134+
135+
self._check_result(result)
135136
else:
136137
try:
137138
result = app.acquire_token_interactive(
@@ -157,16 +158,19 @@ def _request_token(self, *scopes: str, **kwargs: Any) -> Dict:
157158
parent_window_handle=self._parent_window_handle,
158159
enable_msa_passthrough=self._enable_msa_passthrough,
159160
)
160-
if "access_token" in result:
161-
return result
162-
if "error_description" in result:
163-
if within_dac.get():
164-
# pylint: disable=raise-missing-from
165-
raise CredentialUnavailableError(message=result["error_description"])
166-
# pylint: disable=raise-missing-from
167-
raise ClientAuthenticationError(message=result.get("error_description"))
161+
self._check_result(result)
168162
return result
169163

164+
def _check_result(self, result: Dict[str, Any]) -> None:
165+
if "access_token" not in result and "error_description" in result:
166+
if within_dac.get():
167+
raise CredentialUnavailableError(message=result["error_description"])
168+
raise ClientAuthenticationError(message=result.get("error_description"))
169+
if "access_token" not in result:
170+
if within_dac.get():
171+
raise CredentialUnavailableError(message="Failed to authenticate user")
172+
raise ClientAuthenticationError(message="Failed to authenticate user")
173+
170174
def _get_app(self, **kwargs: Any) -> msal.ClientApplication:
171175
tenant_id = resolve_tenant(
172176
self._tenant_id, additionally_allowed_tenants=self._additionally_allowed_tenants, **kwargs

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

sdk/identity/azure-identity/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ The Azure Identity library offers both in-memory and persistent disk caching. Fo
321321

322322
## Brokered authentication
323323

324-
An authentication broker is an application that runs on a user’s machine and manages the authentication handshakes and token maintenance for connected accounts. Currently, only the Windows Web Account Manager (WAM) is supported. To enable support, use the [`azure-identity-broker`][azure_identity_broker] package. For details on authenticating using WAM, see the [broker plugin documentation][azure_identity_broker_readme].
324+
An authentication broker is an application that runs on a user’s machine and manages the authentication handshakes and token maintenance for connected accounts. Currently, only the Windows Web Account Manager (WAM) is supported. Authentication via WAM is used by `DefaultAzureCredential` to enable secure sign-in. To enable support, use the [`azure-identity-broker`][azure_identity_broker] package. For details on authenticating using WAM, see the [broker plugin documentation][azure_identity_broker_readme].
325325

326326
## Troubleshooting
327327

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
"disable_interactive_fallback": True,
39+
**kwargs,
40+
}
41+
if self._client_id:
42+
broker_credential_args["client_id"] = self._client_id
43+
self._broker_credential = broker_credential_class(**broker_credential_args)
44+
except Exception as ex: # pylint: disable=broad-except
45+
self._unavailable_message = f"InteractiveBrowserBrokerCredential initialization failed: {ex}"
46+
else:
47+
# Determine the specific reason for unavailability
48+
if broker_credential_class is None:
49+
self._unavailable_message = (
50+
"InteractiveBrowserBrokerCredential unavailable. "
51+
"The 'azure-identity-broker' package is required to use brokered authentication."
52+
)
53+
else:
54+
self._unavailable_message = (
55+
"InteractiveBrowserBrokerCredential unavailable. "
56+
"Brokered authentication is only supported on Windows and WSL platforms."
57+
)
58+
59+
def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
60+
if self._broker_credential:
61+
return self._broker_credential.get_token(*scopes, **kwargs)
62+
raise CredentialUnavailableError(message=self._unavailable_message)
63+
64+
def get_token_info(self, *scopes: str, **kwargs: Any) -> AccessTokenInfo:
65+
if self._broker_credential:
66+
return self._broker_credential.get_token_info(*scopes, **kwargs)
67+
raise CredentialUnavailableError(message=self._unavailable_message)
68+
69+
def __enter__(self) -> "BrokerCredential":
70+
if self._broker_credential:
71+
self._broker_credential.__enter__()
72+
return self
73+
74+
def __exit__(self, *args):
75+
if self._broker_credential:
76+
self._broker_credential.__exit__(*args)
77+
78+
def close(self) -> None:
79+
self.__exit__()

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

Lines changed: 25 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,8 @@ 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 only, this uses the default account logged in via
47+
Web Account Manager (WAM) if the `azure-identity-broker` package is installed.
4548
4649
This default behavior is configurable with keyword arguments.
4750
@@ -64,9 +67,13 @@ class DefaultAzureCredential(ChainedTokenCredential):
6467
**False**.
6568
:keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see
6669
:class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**.
70+
:keyword bool exclude_broker_credential: Whether to exclude the broker credential from the credential chain.
71+
Defaults to **False**.
6772
:keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through
6873
:class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable
6974
AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
75+
:keyword str broker_tenant_id: The tenant ID to use when using brokered authentication. Defaults to the value of
76+
environment variable AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants.
7077
:keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value
7178
of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used.
7279
:keyword str workload_identity_client_id: The client ID of an identity assigned to the pod. Defaults to the value
@@ -75,6 +82,8 @@ class DefaultAzureCredential(ChainedTokenCredential):
7582
Defaults to the value of environment variable AZURE_TENANT_ID, if any.
7683
:keyword str interactive_browser_client_id: The client ID to be used in interactive browser credential. If not
7784
specified, users will authenticate to an Azure development application.
85+
:keyword str broker_client_id: The client ID to be used in brokered authentication. If not specified, users will
86+
authenticate to an Azure development application.
7887
:keyword str shared_cache_username: Preferred username for :class:`~azure.identity.SharedTokenCacheCredential`.
7988
Defaults to the value of environment variable AZURE_USERNAME, if any.
8089
:keyword str shared_cache_tenant_id: Preferred tenant for :class:`~azure.identity.SharedTokenCacheCredential`.
@@ -117,6 +126,9 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
117126
)
118127
interactive_browser_client_id = kwargs.pop("interactive_browser_client_id", None)
119128

129+
broker_tenant_id = kwargs.pop("broker_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID))
130+
broker_client_id = kwargs.pop("broker_client_id", None)
131+
120132
shared_cache_username = kwargs.pop("shared_cache_username", os.environ.get(EnvironmentVariables.AZURE_USERNAME))
121133
shared_cache_tenant_id = kwargs.pop(
122134
"shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID)
@@ -147,6 +159,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
147159
},
148160
"visual_studio_code": {
149161
"exclude_param": "exclude_visual_studio_code_credential",
162+
"env_name": "visualstudiocodecredential",
150163
"default_exclude": False,
151164
},
152165
"cli": {
@@ -169,6 +182,10 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
169182
"env_name": "interactivebrowsercredential",
170183
"default_exclude": True,
171184
},
185+
"broker": {
186+
"exclude_param": "exclude_broker_credential",
187+
"default_exclude": False,
188+
},
172189
}
173190

174191
# Extract user-provided exclude flags and set defaults
@@ -192,6 +209,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
192209
exclude_developer_cli_credential = exclude_flags["developer_cli"]
193210
exclude_powershell_credential = exclude_flags["powershell"]
194211
exclude_interactive_browser_credential = exclude_flags["interactive_browser"]
212+
exclude_broker_credential = exclude_flags["broker"]
195213

196214
credentials: List[SupportsTokenInfo] = []
197215
within_dac.set(True)
@@ -242,6 +260,12 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement
242260
)
243261
else:
244262
credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs))
263+
if not exclude_broker_credential:
264+
broker_credential_args = {"tenant_id": broker_tenant_id, **kwargs}
265+
if broker_client_id:
266+
broker_credential_args["client_id"] = broker_client_id
267+
credentials.append(BrokerCredential(**broker_credential_args))
268+
245269
within_dac.set(False)
246270
super(DefaultAzureCredential, self).__init__(*credentials)
247271

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

0 commit comments

Comments
 (0)