Skip to content

Commit 86f448b

Browse files
authored
Merge branch 'dev' into mi-arc-detection
2 parents e9b3913 + 7db6c2c commit 86f448b

File tree

14 files changed

+617
-133
lines changed

14 files changed

+617
-133
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ docs/_build/
5757
# The test configuration file(s) could potentially contain credentials
5858
tests/config.json
5959

60+
# Token Cache files
61+
msal_cache.bin
6062

6163
.env
6264
.perf.baseline

msal/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ def _main():
299299
authority=authority,
300300
instance_discovery=instance_discovery,
301301
enable_broker_on_windows=enable_broker,
302+
enable_broker_on_mac=enable_broker,
302303
enable_pii_log=enable_pii_log,
303304
token_cache=global_cache,
304305
) if not is_cca else msal.ConfidentialClientApplication(

msal/application.py

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import warnings
77
from threading import Lock
8+
from typing import Optional # Needed in Python 3.7 & 3.8
89
import os
910

1011
from .oauth2cli import Client, JwtAssertionCreator
@@ -21,11 +22,16 @@
2122

2223

2324
# The __init__.py will import this. Not the other way around.
24-
__version__ = "1.30.0" # When releasing, also check and bump our dependencies's versions if needed
25+
__version__ = "1.31.0" # When releasing, also check and bump our dependencies's versions if needed
2526

2627
logger = logging.getLogger(__name__)
2728
_AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
2829

30+
def _init_broker(enable_pii_log): # Make it a function to allow mocking
31+
from . import broker # Trigger Broker's initialization, lazily
32+
if enable_pii_log:
33+
broker._enable_pii_log()
34+
2935
def extract_certs(public_cert_content):
3036
# Parses raw public certificate file contents and returns a list of strings
3137
# Usage: headers = {"x5c": extract_certs(open("my_cert.pem").read())}
@@ -189,6 +195,21 @@ def obtain_token_by_username_password(self, username, password, **kwargs):
189195
username, password, headers=headers, **kwargs)
190196

191197

198+
def _msal_extension_check():
199+
# Can't run this in module or class level otherwise you'll get circular import error
200+
try:
201+
from msal_extensions import __version__ as v
202+
major, minor, _ = v.split(".", maxsplit=3)
203+
if not (int(major) >= 1 and int(minor) >= 2):
204+
warnings.warn(
205+
"Please upgrade msal-extensions. "
206+
"Only msal-extensions 1.2+ can work with msal 1.30+")
207+
except ImportError:
208+
pass # The optional msal_extensions is not installed. Business as usual.
209+
except ValueError:
210+
logger.exception(f"msal_extensions version {v} not in major.minor.patch format")
211+
212+
192213
class ClientApplication(object):
193214
"""You do not usually directly use this class. Use its subclasses instead:
194215
:class:`PublicClientApplication` and :class:`ConfidentialClientApplication`.
@@ -205,6 +226,7 @@ class ClientApplication(object):
205226
REMOVE_ACCOUNT_ID = "903"
206227

207228
ATTEMPT_REGION_DISCOVERY = True # "TryAutoDetect"
229+
DISABLE_MSAL_FORCE_REGION = False # Used in azure_region to disable MSAL_FORCE_REGION behavior
208230
_TOKEN_SOURCE = "token_source"
209231
_TOKEN_SOURCE_IDP = "identity_provider"
210232
_TOKEN_SOURCE_CACHE = "cache"
@@ -411,9 +433,11 @@ def __init__(
411433
(STS) what this client is capable for,
412434
so STS can decide to turn on certain features.
413435
For example, if client is capable to handle *claims challenge*,
414-
STS can then issue CAE access tokens to resources
415-
knowing when the resource emits *claims challenge*
416-
the client will be capable to handle.
436+
STS may issue
437+
`Continuous Access Evaluation (CAE) <https://learn.microsoft.com/entra/identity/conditional-access/concept-continuous-access-evaluation>`_
438+
access tokens to resources,
439+
knowing that when the resource emits a *claims challenge*
440+
the client will be able to handle those challenges.
417441
418442
Implementation details:
419443
Client capability is implemented using "claims" parameter on the wire,
@@ -426,11 +450,14 @@ def __init__(
426450
Instructs MSAL to use the Entra regional token service. This legacy feature is only available to
427451
first-party applications. Only ``acquire_token_for_client()`` is supported.
428452
429-
Supports 3 values:
453+
Supports 4 values:
430454
431-
``azure_region=None`` - meaning no region is used. This is the default value.
432-
``azure_region="some_region"`` - meaning the specified region is used.
433-
``azure_region=True`` - meaning MSAL will try to auto-detect the region. This is not recommended.
455+
1. ``azure_region=None`` - This default value means no region is configured.
456+
MSAL will use the region defined in env var ``MSAL_FORCE_REGION``.
457+
2. ``azure_region="some_region"`` - meaning the specified region is used.
458+
3. ``azure_region=True`` - meaning
459+
MSAL will try to auto-detect the region. This is not recommended.
460+
4. ``azure_region=False`` - meaning MSAL will use no region.
434461
435462
.. note::
436463
Region auto-discovery has been tested on VMs and on Azure Functions. It is unreliable.
@@ -608,7 +635,10 @@ def __init__(
608635
except ValueError: # Those are explicit authority validation errors
609636
raise
610637
except Exception: # The rest are typically connection errors
611-
if validate_authority and azure_region and not oidc_authority:
638+
if validate_authority and not oidc_authority and (
639+
azure_region # Opted in to use region
640+
or (azure_region is None and os.getenv("MSAL_FORCE_REGION")) # Will use region
641+
):
612642
# Since caller opts in to use region, here we tolerate connection
613643
# errors happened during authority validation at non-region endpoint
614644
self.authority = Authority(
@@ -628,6 +658,8 @@ def __init__(
628658
self.authority_groups = None
629659
self._telemetry_buffer = {}
630660
self._telemetry_lock = Lock()
661+
_msal_extension_check()
662+
631663

632664
def _decide_broker(self, allow_broker, enable_pii_log):
633665
is_confidential_app = self.client_credential or isinstance(
@@ -638,20 +670,28 @@ def _decide_broker(self, allow_broker, enable_pii_log):
638670
if allow_broker:
639671
warnings.warn(
640672
"allow_broker is deprecated. "
641-
"Please use PublicClientApplication(..., enable_broker_on_windows=True)",
673+
"Please use PublicClientApplication(..., "
674+
"enable_broker_on_windows=True, "
675+
"enable_broker_on_mac=...)",
642676
DeprecationWarning)
643-
self._enable_broker = self._enable_broker or (
677+
opted_in_for_broker = (
678+
self._enable_broker # True means Opted-in from PCA
679+
or (
644680
# When we started the broker project on Windows platform,
645681
# the allow_broker was meant to be cross-platform. Now we realize
646682
# that other platforms have different redirect_uri requirements,
647683
# so the old allow_broker is deprecated and will only for Windows.
648684
allow_broker and sys.platform == "win32")
649-
if (self._enable_broker and not is_confidential_app
650-
and not self.authority.is_adfs and not self.authority._is_b2c):
685+
)
686+
self._enable_broker = ( # This same variable will also store the state
687+
opted_in_for_broker
688+
and not is_confidential_app
689+
and not self.authority.is_adfs
690+
and not self.authority._is_b2c
691+
)
692+
if self._enable_broker:
651693
try:
652-
from . import broker # Trigger Broker's initialization
653-
if enable_pii_log:
654-
broker._enable_pii_log()
694+
_init_broker(enable_pii_log)
655695
except RuntimeError:
656696
self._enable_broker = False
657697
logger.exception(
@@ -692,9 +732,11 @@ def _build_telemetry_context(
692732
self._telemetry_buffer, self._telemetry_lock, api_id,
693733
correlation_id=correlation_id, refresh_reason=refresh_reason)
694734

695-
def _get_regional_authority(self, central_authority):
696-
if not self._region_configured: # User did not opt-in to ESTS-R
735+
def _get_regional_authority(self, central_authority) -> Optional[Authority]:
736+
if self._region_configured is False: # User opts out of ESTS-R
697737
return None # Short circuit to completely bypass region detection
738+
if self._region_configured is None: # User did not make an ESTS-R choice
739+
self._region_configured = os.getenv("MSAL_FORCE_REGION") or None
698740
self._region_detected = self._region_detected or _detect_region(
699741
self.http_client if self._region_configured is not None else None)
700742
if (self._region_configured != self.ATTEMPT_REGION_DISCOVERY
@@ -1879,7 +1921,7 @@ def __init__(self, client_id, client_credential=None, **kwargs):
18791921
18801922
.. note::
18811923
1882-
You may set enable_broker_on_windows to True.
1924+
You may set enable_broker_on_windows and/or enable_broker_on_mac to True.
18831925
18841926
**What is a broker, and why use it?**
18851927
@@ -1905,9 +1947,11 @@ def __init__(self, client_id, client_credential=None, **kwargs):
19051947
19061948
* ``ms-appx-web://Microsoft.AAD.BrokerPlugin/your_client_id``
19071949
if your app is expected to run on Windows 10+
1950+
* ``msauth.com.msauth.unsignedapp://auth``
1951+
if your app is expected to run on Mac
19081952
19091953
2. installed broker dependency,
1910-
e.g. ``pip install msal[broker]>=1.25,<2``.
1954+
e.g. ``pip install msal[broker]>=1.31,<2``.
19111955
19121956
3. tested with ``acquire_token_interactive()`` and ``acquire_token_silent()``.
19131957
@@ -1939,12 +1983,21 @@ def __init__(self, client_id, client_credential=None, **kwargs):
19391983
This parameter defaults to None, which means MSAL will not utilize a broker.
19401984
19411985
New in MSAL Python 1.25.0.
1986+
1987+
:param boolean enable_broker_on_mac:
1988+
This setting is only effective if your app is running on Mac.
1989+
This parameter defaults to None, which means MSAL will not utilize a broker.
1990+
1991+
New in MSAL Python 1.31.0.
19421992
"""
19431993
if client_credential is not None:
19441994
raise ValueError("Public Client should not possess credentials")
19451995
# Using kwargs notation for now. We will switch to keyword-only arguments.
19461996
enable_broker_on_windows = kwargs.pop("enable_broker_on_windows", False)
1947-
self._enable_broker = enable_broker_on_windows and sys.platform == "win32"
1997+
enable_broker_on_mac = kwargs.pop("enable_broker_on_mac", False)
1998+
self._enable_broker = bool(
1999+
enable_broker_on_windows and sys.platform == "win32"
2000+
or enable_broker_on_mac and sys.platform == "darwin")
19482001
super(PublicClientApplication, self).__init__(
19492002
client_id, client_credential=None, **kwargs)
19502003

@@ -2022,14 +2075,22 @@ def acquire_token_interactive(
20222075
New in version 1.15.
20232076
20242077
:param int parent_window_handle:
2025-
Required if your app is running on Windows and opted in to use broker.
2078+
OPTIONAL.
2079+
2080+
* If your app does not opt in to use broker,
2081+
you do not need to provide a ``parent_window_handle`` here.
2082+
2083+
* If your app opts in to use broker,
2084+
``parent_window_handle`` is required.
20262085
2027-
If your app is a GUI app,
2028-
you are recommended to also provide its window handle,
2029-
so that the sign in UI window will properly pop up on top of your window.
2086+
- If your app is a GUI app running on Windows or Mac system,
2087+
you are required to also provide its window handle,
2088+
so that the sign-in window will pop up on top of your window.
2089+
- If your app is a console app running on Windows or Mac system,
2090+
you can use a placeholder
2091+
``PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
20302092
2031-
If your app is a console app (most Python scripts are console apps),
2032-
you can use a placeholder value ``msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE``.
2093+
Most Python scripts are console apps.
20332094
20342095
New in version 1.20.0.
20352096

msal/broker.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""This module is an adaptor to the underlying broker.
22
It relies on PyMsalRuntime which is the package providing broker's functionality.
33
"""
4-
from threading import Event
54
import json
65
import logging
6+
import sys
77
import time
88
import uuid
99

@@ -23,7 +23,15 @@
2323
except (ImportError, AttributeError): # AttributeError happens when a prior pymsalruntime uninstallation somehow leaved an empty folder behind
2424
# PyMsalRuntime currently supports these Windows versions, listed in this MSFT internal link
2525
# https://github.com/AzureAD/microsoft-authentication-library-for-cpp/pull/2406/files
26-
raise ImportError('You need to install dependency by: pip install "msal[broker]>=1.20,<2"')
26+
min_ver = {
27+
"win32": "1.20",
28+
"darwin": "1.31",
29+
}.get(sys.platform)
30+
if min_ver:
31+
raise ImportError(
32+
f'You must install dependency by: pip install "msal[broker]>={min_ver},<2"')
33+
else: # Unsupported platform
34+
raise ImportError("Dependency pymsalruntime unavailable on current platform")
2735
# It could throw RuntimeError when running on ancient versions of Windows
2836

2937

@@ -35,14 +43,12 @@ class TokenTypeError(ValueError):
3543
pass
3644

3745

38-
class _CallbackData:
39-
def __init__(self):
40-
self.signal = Event()
41-
self.result = None
42-
43-
def complete(self, result):
44-
self.signal.set()
45-
self.result = result
46+
_redirect_uri_on_mac = "msauth.com.msauth.unsignedapp://auth" # Note:
47+
# On Mac, the native Python has a team_id which links to bundle id
48+
# com.apple.python3 however it won't give Python scripts better security.
49+
# Besides, the homebrew-installed Pythons have no team_id
50+
# so they have to use a generic placeholder anyway.
51+
# The v-team chose to combine two situations into using same placeholder.
4652

4753

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

7178

7279
def _read_account_by_id(account_id, correlation_id):
73-
"""Return an instance of MSALRuntimeAccount, or log error and return None"""
74-
callback_data = _CallbackData()
80+
"""Return an instance of MSALRuntimeError or MSALRuntimeAccount, or None"""
81+
callback_data = pymsalruntime.CallbackData()
7582
pymsalruntime.read_account_by_id(
7683
account_id,
7784
correlation_id,
@@ -142,7 +149,7 @@ def _signin_silently(
142149
params.set_pop_params(
143150
auth_scheme._http_method, auth_scheme._url.netloc, auth_scheme._url.path,
144151
auth_scheme._nonce)
145-
callback_data = _CallbackData()
152+
callback_data = pymsalruntime.CallbackData()
146153
for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc.
147154
if v is not None:
148155
params.set_additional_parameter(k, str(v))
@@ -169,9 +176,12 @@ def _signin_interactively(
169176
**kwargs):
170177
params = pymsalruntime.MSALRuntimeAuthParameters(client_id, authority)
171178
params.set_requested_scopes(scopes)
172-
params.set_redirect_uri("https://login.microsoftonline.com/common/oauth2/nativeclient")
173-
# This default redirect_uri value is not currently used by the broker
179+
params.set_redirect_uri(
180+
_redirect_uri_on_mac if sys.platform == "darwin" else
181+
"https://login.microsoftonline.com/common/oauth2/nativeclient"
182+
# This default redirect_uri value is not currently used by WAM
174183
# but it is required by the MSAL.cpp to be set to a non-empty valid URI.
184+
)
175185
if prompt:
176186
if prompt == "select_account":
177187
if login_hint:
@@ -198,7 +208,7 @@ def _signin_interactively(
198208
params.set_additional_parameter(k, str(v))
199209
if claims:
200210
params.set_decoded_claims(claims)
201-
callback_data = _CallbackData()
211+
callback_data = pymsalruntime.CallbackData(is_interactive=True)
202212
pymsalruntime.signin_interactively(
203213
parent_window_handle or pymsalruntime.get_console_window() or pymsalruntime.get_desktop_window(), # Since pymsalruntime 0.2+
204214
params,
@@ -231,7 +241,7 @@ def _acquire_token_silently(
231241
for k, v in kwargs.items(): # This can be used to support domain_hint, max_age, etc.
232242
if v is not None:
233243
params.set_additional_parameter(k, str(v))
234-
callback_data = _CallbackData()
244+
callback_data = pymsalruntime.CallbackData()
235245
pymsalruntime.acquire_token_silently(
236246
params,
237247
correlation_id,
@@ -247,7 +257,7 @@ def _signout_silently(client_id, account_id, correlation_id=None):
247257
account = _read_account_by_id(account_id, correlation_id)
248258
if account is None:
249259
return
250-
callback_data = _CallbackData()
260+
callback_data = pymsalruntime.CallbackData()
251261
pymsalruntime.signout_silently( # New in PyMsalRuntime 0.7
252262
client_id,
253263
correlation_id,

0 commit comments

Comments
 (0)