Skip to content

Commit 403a33d

Browse files
committed
Merge branch 'release-1.26.0'
2 parents 3bd70b9 + 35310b5 commit 403a33d

File tree

7 files changed

+299
-87
lines changed

7 files changed

+299
-87
lines changed

msal/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@
3333
)
3434
from .oauth2cli.oidc import Prompt
3535
from .token_cache import TokenCache, SerializableTokenCache
36+
from .auth_scheme import PopAuthScheme
3637

msal/__main__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222

2323
_AZURE_CLI = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
2424
_VISUAL_STUDIO = "04f0c124-f2bc-4f59-8241-bf6df9866bbd"
25+
placeholder_auth_scheme = msal.PopAuthScheme(
26+
http_method=msal.PopAuthScheme.HTTP_GET,
27+
url="https://example.com/endpoint",
28+
nonce="placeholder",
29+
)
2530

2631
def print_json(blob):
2732
print(json.dumps(blob, indent=2, sort_keys=True))
@@ -88,6 +93,9 @@ def _acquire_token_silent(app):
8893
_input_scopes(),
8994
account=account,
9095
force_refresh=_input_boolean("Bypass MSAL Python's token cache?"),
96+
auth_scheme=placeholder_auth_scheme
97+
if app.is_pop_supported() and _input_boolean("Acquire AT POP via Broker?")
98+
else None,
9199
))
92100

93101
def _acquire_token_interactive(app, scopes=None, data=None):
@@ -117,6 +125,9 @@ def _acquire_token_interactive(app, scopes=None, data=None):
117125
], # Here this test app mimics the setting for some known MSA-PT apps
118126
port=1234, # Hard coded for testing. Real app typically uses default value.
119127
prompt=prompt, login_hint=login_hint, data=data or {},
128+
auth_scheme=placeholder_auth_scheme
129+
if app.is_pop_supported() and _input_boolean("Acquire AT POP via Broker?")
130+
else None,
120131
)
121132
if login_hint and "id_token_claims" in result:
122133
signed_in_user = result.get("id_token_claims", {}).get("preferred_username")

msal/application.py

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626

2727
# The __init__.py will import this. Not the other way around.
28-
__version__ = "1.25.0" # When releasing, also check and bump our dependencies's versions if needed
28+
__version__ = "1.26.0" # When releasing, also check and bump our dependencies's versions if needed
2929

3030
logger = logging.getLogger(__name__)
3131
_AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
@@ -182,6 +182,10 @@ class ClientApplication(object):
182182
_TOKEN_SOURCE_BROKER = "broker"
183183

184184
_enable_broker = False
185+
_AUTH_SCHEME_UNSUPPORTED = (
186+
"auth_scheme is currently only available from broker. "
187+
"You can enable broker by following these instructions. "
188+
"https://msal-python.readthedocs.io/en/latest/#publicclientapplication")
185189

186190
def __init__(
187191
self, client_id,
@@ -336,51 +340,22 @@ def __init__(
336340
`claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter>`_
337341
which you will later provide via one of the acquire-token request.
338342
339-
:param str azure_region:
340-
AAD provides regional endpoints for apps to opt in
341-
to keep their traffic remain inside that region.
343+
:param str azure_region: (optional)
344+
Instructs MSAL to use the Entra regional token service. This legacy feature is only available to
345+
first-party applications. Only ``acquire_token_for_client()`` is supported.
342346
343-
As of 2021 May, regional service is only available for
344-
``acquire_token_for_client()`` sent by any of the following scenarios:
347+
Supports 3 values:
345348
346-
1. An app powered by a capable MSAL
347-
(MSAL Python 1.12+ will be provisioned)
348-
349-
2. An app with managed identity, which is formerly known as MSI.
350-
(However MSAL Python does not support managed identity,
351-
so this one does not apply.)
352-
353-
3. An app authenticated by
354-
`Subject Name/Issuer (SNI) <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_.
355-
356-
4. An app which already onboard to the region's allow-list.
357-
358-
This parameter defaults to None, which means region behavior remains off.
359-
360-
App developer can opt in to a regional endpoint,
361-
by provide its region name, such as "westus", "eastus2".
362-
You can find a full list of regions by running
363-
``az account list-locations -o table``, or referencing to
364-
`this doc <https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.management.resourcemanager.fluent.core.region?view=azure-dotnet>`_.
365-
366-
An app running inside Azure Functions and Azure VM can use a special keyword
367-
``ClientApplication.ATTEMPT_REGION_DISCOVERY`` to auto-detect region.
349+
``azure_region=None`` - meaning no region is used. This is the default value.
350+
``azure_region="some_region"`` - meaning the specified region is used.
351+
``azure_region=True`` - meaning MSAL will try to auto-detect the region. This is not recommended.
368352
369353
.. note::
354+
Region auto-discovery has been tested on VMs and on Azure Functions. It is unreliable.
355+
Applications using this option should configure a short timeout.
370356
371-
Setting ``azure_region`` to non-``None`` for an app running
372-
outside of Azure Function/VM could hang indefinitely.
373-
374-
You should consider opting in/out region behavior on-demand,
375-
by loading ``azure_region=None`` or ``azure_region="westus"``
376-
or ``azure_region=True`` (which means opt-in and auto-detect)
377-
from your per-deployment configuration, and then do
378-
``app = ConfidentialClientApplication(..., azure_region=azure_region)``.
379-
380-
Alternatively, you can configure a short timeout,
381-
or provide a custom http_client which has a short timeout.
382-
That way, the latency would be under your control,
383-
but still less performant than opting out of region feature.
357+
For more details and for the values of the region string
358+
see https://learn.microsoft.com/entra/msal/dotnet/resources/region-discovery-troubleshooting
384359
385360
New in version 1.12.0.
386361
@@ -586,6 +561,10 @@ def _decide_broker(self, allow_broker, enable_pii_log):
586561
"We will fallback to non-broker.")
587562
logger.debug("Broker enabled? %s", self._enable_broker)
588563

564+
def is_pop_supported(self):
565+
"""Returns True if this client supports Proof-of-Possession Access Token."""
566+
return self._enable_broker
567+
589568
def _decorate_scope(
590569
self, scopes,
591570
reserved_scope=frozenset(['openid', 'profile', 'offline_access'])):
@@ -612,6 +591,8 @@ def _build_telemetry_context(
612591
correlation_id=correlation_id, refresh_reason=refresh_reason)
613592

614593
def _get_regional_authority(self, central_authority):
594+
if not self._region_configured: # User did not opt-in to ESTS-R
595+
return None # Short circuit to completely bypass region detection
615596
self._region_detected = self._region_detected or _detect_region(
616597
self.http_client if self._region_configured is not None else None)
617598
if (self._region_configured != self.ATTEMPT_REGION_DISCOVERY
@@ -1212,6 +1193,7 @@ def acquire_token_silent(
12121193
authority=None, # See get_authorization_request_url()
12131194
force_refresh=False, # type: Optional[boolean]
12141195
claims_challenge=None,
1196+
auth_scheme=None,
12151197
**kwargs):
12161198
"""Acquire an access token for given account, without user interaction.
12171199
@@ -1232,7 +1214,7 @@ def acquire_token_silent(
12321214
return None # A backward-compatible NO-OP to drop the account=None usage
12331215
result = _clean_up(self._acquire_token_silent_with_error(
12341216
scopes, account, authority=authority, force_refresh=force_refresh,
1235-
claims_challenge=claims_challenge, **kwargs))
1217+
claims_challenge=claims_challenge, auth_scheme=auth_scheme, **kwargs))
12361218
return result if result and "error" not in result else None
12371219

12381220
def acquire_token_silent_with_error(
@@ -1242,6 +1224,7 @@ def acquire_token_silent_with_error(
12421224
authority=None, # See get_authorization_request_url()
12431225
force_refresh=False, # type: Optional[boolean]
12441226
claims_challenge=None,
1227+
auth_scheme=None,
12451228
**kwargs):
12461229
"""Acquire an access token for given account, without user interaction.
12471230
@@ -1268,6 +1251,12 @@ def acquire_token_silent_with_error(
12681251
in the form of a claims_challenge directive in the www-authenticate header to be
12691252
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
12701253
It is a string of a JSON object which contains lists of claims being requested from these locations.
1254+
:param object auth_scheme:
1255+
You can provide an ``msal.auth_scheme.PopAuthScheme`` object
1256+
so that MSAL will get a Proof-of-Possession (POP) token for you.
1257+
1258+
New in version 1.26.0.
1259+
12711260
:return:
12721261
- A dict containing no "error" key,
12731262
and typically contains an "access_token" key,
@@ -1279,7 +1268,7 @@ def acquire_token_silent_with_error(
12791268
return None # A backward-compatible NO-OP to drop the account=None usage
12801269
return _clean_up(self._acquire_token_silent_with_error(
12811270
scopes, account, authority=authority, force_refresh=force_refresh,
1282-
claims_challenge=claims_challenge, **kwargs))
1271+
claims_challenge=claims_challenge, auth_scheme=auth_scheme, **kwargs))
12831272

12841273
def _acquire_token_silent_with_error(
12851274
self,
@@ -1288,6 +1277,7 @@ def _acquire_token_silent_with_error(
12881277
authority=None, # See get_authorization_request_url()
12891278
force_refresh=False, # type: Optional[boolean]
12901279
claims_challenge=None,
1280+
auth_scheme=None,
12911281
**kwargs):
12921282
assert isinstance(scopes, list), "Invalid parameter type"
12931283
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
@@ -1303,6 +1293,7 @@ def _acquire_token_silent_with_error(
13031293
scopes, account, self.authority, force_refresh=force_refresh,
13041294
claims_challenge=claims_challenge,
13051295
correlation_id=correlation_id,
1296+
auth_scheme=auth_scheme,
13061297
**kwargs)
13071298
if result and "error" not in result:
13081299
return result
@@ -1325,6 +1316,7 @@ def _acquire_token_silent_with_error(
13251316
scopes, account, the_authority, force_refresh=force_refresh,
13261317
claims_challenge=claims_challenge,
13271318
correlation_id=correlation_id,
1319+
auth_scheme=auth_scheme,
13281320
**kwargs)
13291321
if result:
13301322
if "error" not in result:
@@ -1349,12 +1341,13 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
13491341
claims_challenge=None,
13501342
correlation_id=None,
13511343
http_exceptions=None,
1344+
auth_scheme=None,
13521345
**kwargs):
13531346
# This internal method has two calling patterns:
13541347
# it accepts a non-empty account to find token for a user,
13551348
# and accepts account=None to find a token for the current app.
13561349
access_token_from_cache = None
1357-
if not (force_refresh or claims_challenge): # Bypass AT when desired or using claims
1350+
if not (force_refresh or claims_challenge or auth_scheme): # Then attempt AT cache
13581351
query={
13591352
"client_id": self.client_id,
13601353
"environment": authority.instance,
@@ -1397,6 +1390,8 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
13971390
try:
13981391
data = kwargs.get("data", {})
13991392
if account and account.get("authority_type") == _AUTHORITY_TYPE_CLOUDSHELL:
1393+
if auth_scheme:
1394+
raise ValueError("auth_scheme is not supported in Cloud Shell")
14001395
return self._acquire_token_by_cloud_shell(scopes, data=data)
14011396

14021397
if self._enable_broker and account and account.get("account_source") in (
@@ -1412,6 +1407,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
14121407
claims=_merge_claims_challenge_and_capabilities(
14131408
self._client_capabilities, claims_challenge),
14141409
correlation_id=correlation_id,
1410+
auth_scheme=auth_scheme,
14151411
**data)
14161412
if response: # Broker provides a decisive outcome
14171413
account_was_established_by_broker = account.get(
@@ -1420,6 +1416,8 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
14201416
if account_was_established_by_broker or broker_attempt_succeeded_just_now:
14211417
return self._process_broker_response(response, scopes, data)
14221418

1419+
if auth_scheme:
1420+
raise ValueError(self._AUTH_SCHEME_UNSUPPORTED)
14231421
if account:
14241422
result = self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
14251423
authority, self._decorate_scope(scopes), account,
@@ -1615,7 +1613,11 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
16151613
return response
16161614

16171615
def acquire_token_by_username_password(
1618-
self, username, password, scopes, claims_challenge=None, **kwargs):
1616+
self, username, password, scopes, claims_challenge=None,
1617+
# Note: We shouldn't need to surface enable_msa_passthrough,
1618+
# because this ROPC won't work with MSA account anyway.
1619+
auth_scheme=None,
1620+
**kwargs):
16191621
"""Gets a token for a given resource via user credentials.
16201622
16211623
See this page for constraints of Username Password Flow.
@@ -1631,6 +1633,12 @@ def acquire_token_by_username_password(
16311633
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
16321634
It is a string of a JSON object which contains lists of claims being requested from these locations.
16331635
1636+
:param object auth_scheme:
1637+
You can provide an ``msal.auth_scheme.PopAuthScheme`` object
1638+
so that MSAL will get a Proof-of-Possession (POP) token for you.
1639+
1640+
New in version 1.26.0.
1641+
16341642
:return: A dict representing the json response from AAD:
16351643
16361644
- A successful response would contain "access_token" key,
@@ -1650,9 +1658,12 @@ def acquire_token_by_username_password(
16501658
self.authority._is_known_to_developer
16511659
or self._instance_discovery is False) else None,
16521660
claims=claims,
1661+
auth_scheme=auth_scheme,
16531662
)
16541663
return self._process_broker_response(response, scopes, kwargs.get("data", {}))
16551664

1665+
if auth_scheme:
1666+
raise ValueError(self._AUTH_SCHEME_UNSUPPORTED)
16561667
scopes = self._decorate_scope(scopes)
16571668
telemetry_context = self._build_telemetry_context(
16581669
self.ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID)
@@ -1795,6 +1806,7 @@ def acquire_token_interactive(
17951806
max_age=None,
17961807
parent_window_handle=None,
17971808
on_before_launching_ui=None,
1809+
auth_scheme=None,
17981810
**kwargs):
17991811
"""Acquire token interactively i.e. via a local browser.
18001812
@@ -1870,6 +1882,12 @@ def acquire_token_interactive(
18701882
18711883
New in version 1.20.0.
18721884
1885+
:param object auth_scheme:
1886+
You can provide an ``msal.auth_scheme.PopAuthScheme`` object
1887+
so that MSAL will get a Proof-of-Possession (POP) token for you.
1888+
1889+
New in version 1.26.0.
1890+
18731891
:return:
18741892
- A dict containing no "error" key,
18751893
and typically contains an "access_token" key.
@@ -1914,12 +1932,15 @@ def acquire_token_interactive(
19141932
claims,
19151933
data,
19161934
on_before_launching_ui,
1935+
auth_scheme,
19171936
prompt=prompt,
19181937
login_hint=login_hint,
19191938
max_age=max_age,
19201939
)
19211940
return self._process_broker_response(response, scopes, data)
19221941

1942+
if auth_scheme:
1943+
raise ValueError("auth_scheme is currently only available from broker")
19231944
on_before_launching_ui(ui="browser")
19241945
telemetry_context = self._build_telemetry_context(
19251946
self.ACQUIRE_TOKEN_INTERACTIVE)
@@ -1954,6 +1975,7 @@ def _acquire_token_interactive_via_broker(
19541975
claims, # type: str
19551976
data, # type: dict
19561977
on_before_launching_ui, # type: callable
1978+
auth_scheme, # type: object
19571979
prompt=None,
19581980
login_hint=None, # type: Optional[str]
19591981
max_age=None,
@@ -1977,6 +1999,7 @@ def _acquire_token_interactive_via_broker(
19771999
accounts[0]["local_account_id"],
19782000
scopes,
19792001
claims=claims,
2002+
auth_scheme=auth_scheme,
19802003
**data)
19812004
if response and "error" not in response:
19822005
return response
@@ -1989,6 +2012,7 @@ def _acquire_token_interactive_via_broker(
19892012
claims=claims,
19902013
max_age=max_age,
19912014
enable_msa_pt=enable_msa_passthrough,
2015+
auth_scheme=auth_scheme,
19922016
**data)
19932017
is_wrong_account = bool(
19942018
# _signin_silently() only gets tokens for default account,
@@ -2029,6 +2053,7 @@ def _acquire_token_interactive_via_broker(
20292053
claims=claims,
20302054
max_age=max_age,
20312055
enable_msa_pt=enable_msa_passthrough,
2056+
auth_scheme=auth_scheme,
20322057
**data)
20332058

20342059
def initiate_device_flow(self, scopes=None, **kwargs):
@@ -2176,8 +2201,7 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, claims_challenge=No
21762201
"""
21772202
telemetry_context = self._build_telemetry_context(
21782203
self.ACQUIRE_TOKEN_ON_BEHALF_OF_ID)
2179-
# The implementation is NOT based on Token Exchange
2180-
# https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
2204+
# The implementation is NOT based on Token Exchange (RFC 8693)
21812205
response = _clean_up(self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
21822206
user_assertion,
21832207
self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs

0 commit comments

Comments
 (0)