Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WSL fallback to InteractiveBrowserCredential #17752

Merged
merged 3 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
when instructed to by a Retry-After header
- ManagedIdentityCredential caches tokens correctly

### Added
- `InteractiveBrowserCredential` functions in more WSL environments
([#17615](https://github.com/Azure/azure-sdk-for-python/issues/17615))

## 1.6.0b2 (2021-03-09)
### Breaking Changes
> These changes do not impact the API of stable versions such as 1.5.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import platform
import socket
import subprocess
import webbrowser

from six.moves.urllib_parse import urlparse
Expand Down Expand Up @@ -101,7 +103,7 @@ def _request_token(self, *scopes, **kwargs):
if "auth_uri" not in flow:
raise CredentialUnavailableError("Failed to begin authentication flow")

if not webbrowser.open(flow["auth_uri"]):
if not _open_browser(flow["auth_uri"]):
raise CredentialUnavailableError(message="Failed to open a browser")

# block until the server times out or receives the post-authentication redirect
Expand All @@ -113,3 +115,25 @@ def _request_token(self, *scopes, **kwargs):

# redeem the authorization code for a token
return app.acquire_token_by_auth_code_flow(flow, response, scopes=scopes, claims_challenge=claims)


def _open_browser(url):
opened = webbrowser.open(url)
if not opened:
uname = platform.uname()
system = uname[0].lower()
release = uname[2].lower()
if "microsoft" in release and system == "linux":
kwargs = {}
if platform.python_version() >= "3.3":
kwargs["timeout"] = 5

try:
exit_code = subprocess.call(
["powershell.exe", "-NoProfile", "-Command", 'Start-Process "{}"'.format(url)], **kwargs
)
opened = exit_code == 0
except Exception: # pylint:disable=broad-except
# powershell.exe isn't available, or the subprocess timed out
pass
return opened
83 changes: 81 additions & 2 deletions sdk/identity/azure-identity/tests/test_browser_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
import platform
import random
import socket
import threading
Expand Down Expand Up @@ -168,10 +169,12 @@ def test_redirect_server():


def test_no_browser():
"""The credential should raise CredentialUnavailableError when it can't open a browser"""

transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2)
credential = InteractiveBrowserCredential(client_id="client-id", _server_class=Mock(), transport=transport)
with pytest.raises(ClientAuthenticationError, match=r".*browser.*"):
with patch(WEBBROWSER_OPEN, lambda _: False):
with patch(InteractiveBrowserCredential.__module__ + "._open_browser", lambda _: False):
with pytest.raises(CredentialUnavailableError, match=r".*browser.*"):
credential.get_token("scope")


Expand Down Expand Up @@ -261,3 +264,79 @@ def test_claims_challenge():
assert msal_app.acquire_token_silent_with_error.call_count == 1
args, kwargs = msal_app.acquire_token_silent_with_error.call_args
assert kwargs["claims_challenge"] == expected_claims


@pytest.mark.parametrize(
"uname,is_wsl",
(
(
(
"Linux",
"machine",
"4.4.0-19041-Microsoft",
"#488-Microsoft Mon Sep 01 13:43:00 PST 2020",
"x86_64",
"x86_64",
),
True,
),
(
(
"Linux",
"machine",
"5.4.72-microsoft-standard-WSL2",
"#1 SMP Wed Oct 28 23:40:43 UTC 2020",
"x86_64",
"x86_64",
),
True,
),
(
(
"Linux",
"machine",
"5.3.0-51-generic",
"#44-Ubuntu SMP Wed Apr 22 21:09:44 UTC 2020",
"x86_64",
"x86_64",
),
False,
),
),
)
def test_wsl_fallback(uname, is_wsl):
"""the credential should invoke powershell.exe to open a browser in WSL when webbrowser.open fails"""

auth_uri = "http://localhost"
expected_access_token = "**"
msal_acquire_token_result = dict(
build_aad_response(access_token=expected_access_token, id_token=build_id_token()),
id_token_claims=id_token_claims("issuer", "subject", "audience", upn="upn"),
)
msal_app = Mock(
initiate_auth_code_flow=Mock(return_value={"auth_uri": auth_uri}),
acquire_token_by_auth_code_flow=Mock(return_value=msal_acquire_token_result),
)

transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent")))
credential = InteractiveBrowserCredential(_server_class=Mock(), transport=transport)

with patch(InteractiveBrowserCredential.__module__ + ".subprocess.call") as subprocess_call:
subprocess_call.return_value = 0
with patch(InteractiveBrowserCredential.__module__ + ".platform.uname", lambda: uname):
with patch.object(InteractiveBrowserCredential, "_get_app", lambda _: msal_app):
with patch(WEBBROWSER_OPEN, lambda _: False):
try:
token = credential.get_token("scope")
except CredentialUnavailableError:
assert not is_wsl, "credential should invoke powershell.exe in WSL"
return

assert is_wsl, "credential should raise CredentialUnavailableError when not in WSL"
assert token.token == expected_access_token
assert subprocess_call.call_count == 1
args, kwargs = subprocess_call.call_args
assert args[0][0] == "powershell.exe"
assert auth_uri in args[0][-1]
if platform.python_version() >= "3.3":
assert "timeout" in kwargs