Skip to content
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
1 change: 1 addition & 0 deletions msal/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ def _main():
authority=authority,
instance_discovery=instance_discovery,
enable_broker_on_windows=enable_broker,
enable_broker_on_mac=enable_broker,
enable_pii_log=enable_pii_log,
token_cache=global_cache,
) if not is_cca else msal.ConfidentialClientApplication(
Expand Down
45 changes: 36 additions & 9 deletions msal/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ class ClientApplication(object):
"You can enable broker by following these instructions. "
"https://msal-python.readthedocs.io/en/latest/#publicclientapplication")

_enable_broker = False

def __init__(
self, client_id,
client_credential=None, authority=None, validate_authority=True,
Expand Down Expand Up @@ -640,7 +642,9 @@ def _decide_broker(self, allow_broker, enable_pii_log):
if allow_broker:
warnings.warn(
"allow_broker is deprecated. "
"Please use PublicClientApplication(..., enable_broker_on_windows=True)",
"Please use PublicClientApplication(..., "
"enable_broker_on_windows=True, "
"enable_broker_on_mac=...)",
DeprecationWarning)
self._enable_broker = self._enable_broker or (
# When we started the broker project on Windows platform,
Expand Down Expand Up @@ -1881,7 +1885,7 @@ def __init__(self, client_id, client_credential=None, **kwargs):

.. note::

You may set enable_broker_on_windows to True.
You may set enable_broker_on_windows and/or enable_broker_on_mac to True.

**What is a broker, and why use it?**

Expand All @@ -1907,9 +1911,11 @@ def __init__(self, client_id, client_credential=None, **kwargs):

* ``ms-appx-web://Microsoft.AAD.BrokerPlugin/your_client_id``
if your app is expected to run on Windows 10+
* ``msauth.com.msauth.unsignedapp://auth``
if your app is expected to run on Mac

2. installed broker dependency,
e.g. ``pip install msal[broker]>=1.25,<2``.
e.g. ``pip install msal[broker]>=1.31,<2``.

3. tested with ``acquire_token_interactive()`` and ``acquire_token_silent()``.

Expand Down Expand Up @@ -1941,12 +1947,21 @@ def __init__(self, client_id, client_credential=None, **kwargs):
This parameter defaults to None, which means MSAL will not utilize a broker.

New in MSAL Python 1.25.0.

:param boolean enable_broker_on_mac:
This setting is only effective if your app is running on Mac.
This parameter defaults to None, which means MSAL will not utilize a broker.

New in MSAL Python 1.31.0.
"""
if client_credential is not None:
raise ValueError("Public Client should not possess credentials")
# Using kwargs notation for now. We will switch to keyword-only arguments.
enable_broker_on_windows = kwargs.pop("enable_broker_on_windows", False)
self._enable_broker = enable_broker_on_windows and sys.platform == "win32"
enable_broker_on_mac = kwargs.pop("enable_broker_on_mac", False)
self._enable_broker = bool(
enable_broker_on_windows and sys.platform == "win32"
or enable_broker_on_mac and sys.platform == "darwin")
super(PublicClientApplication, self).__init__(
client_id, client_credential=None, **kwargs)

Expand Down Expand Up @@ -2024,11 +2039,23 @@ def acquire_token_interactive(
New in version 1.15.

:param int parent_window_handle:
Required if your app is running on Windows and opted in to use broker.

If your app is a GUI app,
you are recommended to also provide its window handle,
so that the sign in UI window will properly pop up on top of your window.
OPTIONAL.

* If your app does not opt in to use broker,
you do not need to provide a ``parent_window_handle`` here.

* If your app opts in to use broker,
``parent_window_handle`` is required.

- If your app is a GUI app running on modern Windows system,
you are required to also provide its window handle,
so that the sign-in window will pop up on top of your window.
- If your app is a console app runnong on Windows system,
you can use a placeholder
``PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
- If your app is running on Mac,
you can use a placeholder
``PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
Comment on lines +2042 to +2058
Copy link
Contributor Author

@rayluo rayluo Oct 13, 2023

Choose a reason for hiding this comment

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

FYI, those lines are the changes (or the lack thereof) of the parent_window_handle requirement between Windows and Mac.

The window handle is required on Windows. Such a decision was influenced by the MsalRuntime's desire to make window handle explicit (CC @MSamWils), and now on Mac this parameter remains required. (CC @kaisong1990 )

If the parameter remains required on Windows, do we want to keep it as required on Mac, or do we make it optional on Mac? Will the latter behavior - i.e. sometimes required, sometimes not - be confusing in itself?

Note: Regardless of MSAL's choice, downstream library can make different decision, such as choose to make this parameter optional, by using MSAL's predefined CONSOLE_WINDOW_HANDLE placeholder as a default value. (CC @xiangyan99 )

Choose a reason for hiding this comment

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

Sorry, you mean it is required on Mac because it is required on Windows?

I am a little confused.

Copy link
Contributor Author

@rayluo rayluo Oct 13, 2023

Choose a reason for hiding this comment

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

Sorry, you mean it is required on Mac because it is required on Windows?

I am a little confused.

Put it this way. We speculate that one consistent API across platform would be ideal. Most Python scripts are console apps and they shall ideally be cross-platform. How do we want those app developers to write their source code?

  1. app.acquire_token_interactive(
        ...,
        parent_window_handle=app.CONSOLE_WINDOW_HANDLE,  # Same placeholder across all platforms, even though it can be a no-op on some platforms
        ...)
  2. if sys.platform == "win32":
        app.acquire_token_interactive(
            ...,
            parent_window_handle=app.CONSOLE_WINDOW_HANDLE,  # Required on Windows
        ...)
    else:
        app.acquire_token_interactive(
            ...,
            # No need to specify parent_window_handle at all
        ...)


If your app is a console app (most Python scripts are console apps),
you can use a placeholder value ``msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
Expand Down
40 changes: 21 additions & 19 deletions msal/broker.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""This module is an adaptor to the underlying broker.
It relies on PyMsalRuntime which is the package providing broker's functionality.
"""
from threading import Event
import json
import logging
import sys
import time
import uuid

Expand Down Expand Up @@ -35,14 +35,12 @@ class TokenTypeError(ValueError):
pass


class _CallbackData:
def __init__(self):
self.signal = Event()
self.result = None

def complete(self, result):
self.signal.set()
self.result = result
_redirect_uri_on_mac = "msauth.com.msauth.unsignedapp://auth" # Note:
# On Mac, the native Python has a team_id which links to bundle id
# com.apple.python3 however it won't give Python scripts better security.
# Besides, the homebrew-installed Pythons have no team_id
# so they have to use a generic placeholder anyway.
# The v-team chose to combine two situations into using same placeholder.


def _convert_error(error, client_id):
Expand All @@ -52,8 +50,9 @@ def _convert_error(error, client_id):
or "AADSTS7000218" in context # This "request body must contain ... client_secret" is just a symptom of current app has no WAM redirect_uri
):
raise RedirectUriError( # This would be seen by either the app developer or end user
"MsalRuntime won't work unless this one more redirect_uri is registered to current app: "
"ms-appx-web://Microsoft.AAD.BrokerPlugin/{}".format(client_id))
"MsalRuntime needs the current app to register these redirect_uri "
"(1) ms-appx-web://Microsoft.AAD.BrokerPlugin/{} (2) {}".format(
client_id, _redirect_uri_on_mac))
# OTOH, AAD would emit other errors when other error handling branch was hit first,
# so, the AADSTS50011/RedirectUriError is not guaranteed to happen.
return {
Expand All @@ -70,8 +69,8 @@ def _convert_error(error, client_id):


def _read_account_by_id(account_id, correlation_id):
"""Return an instance of MSALRuntimeAccount, or log error and return None"""
callback_data = _CallbackData()
"""Return an instance of MSALRuntimeError or MSALRuntimeAccount, or None"""
callback_data = pymsalruntime.CallbackData()
pymsalruntime.read_account_by_id(
account_id,
correlation_id,
Expand Down Expand Up @@ -142,7 +141,7 @@ def _signin_silently(
params.set_pop_params(
auth_scheme._http_method, auth_scheme._url.netloc, auth_scheme._url.path,
auth_scheme._nonce)
callback_data = _CallbackData()
callback_data = pymsalruntime.CallbackData()
for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc.
if v is not None:
params.set_additional_parameter(k, str(v))
Expand All @@ -169,9 +168,12 @@ def _signin_interactively(
**kwargs):
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
params.set_requested_scopes(scopes)
params.set_redirect_uri("https://login.microsoftonline.com/common/oauth2/nativeclient")
# This default redirect_uri value is not currently used by the broker
params.set_redirect_uri(
_redirect_uri_on_mac if sys.platform == "darwin" else
"https://login.microsoftonline.com/common/oauth2/nativeclient"
# This default redirect_uri value is not currently used by WAM
# but it is required by the MSAL.cpp to be set to a non-empty valid URI.
)
if prompt:
if prompt == "select_account":
if login_hint:
Expand All @@ -198,7 +200,7 @@ def _signin_interactively(
params.set_additional_parameter(k, str(v))
if claims:
params.set_decoded_claims(claims)
callback_data = _CallbackData()
callback_data = pymsalruntime.CallbackData(is_interactive=True)
pymsalruntime.signin_interactively(
parent_window_handle or pymsalruntime.get_console_window() or pymsalruntime.get_desktop_window(), # Since pymsalruntime 0.2+
params,
Expand Down Expand Up @@ -231,7 +233,7 @@ def _acquire_token_silently(
for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc.
if v is not None:
params.set_additional_parameter(k, str(v))
callback_data = _CallbackData()
callback_data = pymsalruntime.CallbackData()
pymsalruntime.acquire_token_silently(
params,
correlation_id,
Expand All @@ -247,7 +249,7 @@ def _signout_silently(client_id, account_id, correlation_id=None):
account = _read_account_by_id(account_id, correlation_id)
if account is None:
return
callback_data = _CallbackData()
callback_data = pymsalruntime.CallbackData()
pymsalruntime.signout_silently( # New in PyMsalRuntime 0.7
client_id,
correlation_id,
Expand Down
3 changes: 2 additions & 1 deletion sample/interactive_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
authority=os.getenv('AUTHORITY'), # For Entra ID or External ID
oidc_authority=os.getenv('OIDC_AUTHORITY'), # For External ID with custom domain
#enable_broker_on_windows=True, # Opted in. You will be guided to meet the prerequisites, if your app hasn't already
# See also: https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam#wam-value-proposition
#enable_broker_on_mac=True, # Opted in. You will be guided to meet the prerequisites, if your app hasn't already

token_cache=global_token_cache, # Let this app (re)use an existing token cache.
# If absent, ClientApplication will create its own empty token cache
)
Expand Down
8 changes: 5 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ broker =
# The broker is defined as optional dependency,
# so that downstream apps can opt in. The opt-in is needed, partially because
# most existing MSAL Python apps do not have the redirect_uri needed by broker.
# MSAL Python uses a subset of API from PyMsalRuntime 0.13.0+,
# but we still bump the lower bound to 0.13.2+ for its important bugfix (https://github.com/AzureAD/microsoft-authentication-library-for-cpp/pull/3244)
pymsalruntime>=0.13.2,<0.17; python_version>='3.6' and platform_system=='Windows'
#
# We need pymsalruntime.CallbackData introduced in PyMsalRuntime 0.14
pymsalruntime>=0.14,<0.18; python_version>='3.6' and platform_system=='Windows'
# On Mac, PyMsalRuntime 0.17+ is expected to support SSH cert and ROPC
pymsalruntime>=0.17,<0.18; python_version>='3.8' and platform_system=='Darwin'

[options.packages.find]
exclude =
Expand Down
1 change: 1 addition & 0 deletions tests/broker-test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
pca = msal.PublicClientApplication(
_AZURE_CLI,
authority="https://login.microsoftonline.com/organizations",
enable_broker_on_mac=True,
enable_broker_on_windows=True)

def interactive_and_silent(scopes, auth_scheme, data, expected_token_type):
Expand Down
68 changes: 68 additions & 0 deletions tests/smoke-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# How to Smoke Test MSAL Python

The experimental `python -m msal` usage is designed to be an interactive tool,
which can impersonate arbitrary apps and test most of the MSAL Python APIs.
Note that MSAL Python API's behavior is modeled after OIDC behavior in browser,
which are not exactly the same as the broker API's behavior,
despite that the two sets of API happen to have similar names.

Tokens acquired during the tests will be cached by MSAL Python.
MSAL Python uses an in-memory token cache by default.
This test tool, however, saves a token cache snapshot on disk upon each exit,
and you may choose to reuse it or start afresh during start up.

Typical test cases are listed below.

1. The tool starts with an empty token cache.
In this state, acquire_token_silent() shall always return empty result.

2. When testing with broker, apps would need to register a certain redirect_uri
for the test cases below to work.
We will also test an app without the required redirect_uri registration,
MSAL Python shall return a meaningful error message on what URIs to register.

3. Interactive acquire_token_interactive() shall get a token. In particular,

* The prompt=none option shall succeed when there is a default account,
or error out otherwise.
* The prompt=select_account option shall always prompt with an account picker.
* The prompt=absent option shall prompt an account picker UI
if there are multiple accounts available in browser
and none of them is considered a default account.
In such a case, an optional login_hint=`[email protected]`
shall bypass the account picker.

With a broker, the behavior shall largely match the browser behavior,
unless stated otherwise below.

* Broker (PyMsalRuntime) on Mac does not support silent signin,
so the prompt=absent will also always prompt.

4. ROPC (Resource Owner Password Credential, a.k.a. the username password flow).
The acquire_token_by_username_password() is supported by broker on Windows.
As of Oct 2023, it is not yet supported by broker on Mac,
so it will fall back to non-broker behavior.

5. After step 3 or 4, the acquire_token_silently() shall return a token fast,
because that is the same token returned by step 3 or 4, cached in MSAL Python.
We shall also retest this with the force_refresh=True,
a new token shall be obtained,
typically slower than a token served from MSAL Python's token cache.

6. POP token.
POP token is supported via broker.
This tool test the POP token by using a hardcoded Signed Http Request (SHR).
A test is successful if the POP test function return a token with type as POP.

7. SSH Cert.
The interactive test and silent test shall behave similarly to
their non ssh-cert counterparts, only the `token_type` would be different.

8. Test the remove_account() API. It shall always be successful.
This effectively signs out an account from MSAL Python,
we can confirm that by running acquire_token_silent()
and see that account was gone.

The remove_account() shall also sign out from broker (if broker was enabled),
it does not sign out account from browser (even when browser was used).

1 change: 1 addition & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def _build_app(cls,
authority=authority,
http_client=http_client or MinimalHttpClient(),
enable_broker_on_windows=broker_available,
enable_broker_on_mac=broker_available,
)

def _test_username_password(self,
Expand Down