25
25
26
26
27
27
# 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
29
29
30
30
logger = logging .getLogger (__name__ )
31
31
_AUTHORITY_TYPE_CLOUDSHELL = "CLOUDSHELL"
@@ -182,6 +182,10 @@ class ClientApplication(object):
182
182
_TOKEN_SOURCE_BROKER = "broker"
183
183
184
184
_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" )
185
189
186
190
def __init__ (
187
191
self , client_id ,
@@ -336,51 +340,22 @@ def __init__(
336
340
`claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter>`_
337
341
which you will later provide via one of the acquire-token request.
338
342
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 .
342
346
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:
345
348
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.
368
352
369
353
.. 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.
370
356
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
384
359
385
360
New in version 1.12.0.
386
361
@@ -586,6 +561,10 @@ def _decide_broker(self, allow_broker, enable_pii_log):
586
561
"We will fallback to non-broker." )
587
562
logger .debug ("Broker enabled? %s" , self ._enable_broker )
588
563
564
+ def is_pop_supported (self ):
565
+ """Returns True if this client supports Proof-of-Possession Access Token."""
566
+ return self ._enable_broker
567
+
589
568
def _decorate_scope (
590
569
self , scopes ,
591
570
reserved_scope = frozenset (['openid' , 'profile' , 'offline_access' ])):
@@ -612,6 +591,8 @@ def _build_telemetry_context(
612
591
correlation_id = correlation_id , refresh_reason = refresh_reason )
613
592
614
593
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
615
596
self ._region_detected = self ._region_detected or _detect_region (
616
597
self .http_client if self ._region_configured is not None else None )
617
598
if (self ._region_configured != self .ATTEMPT_REGION_DISCOVERY
@@ -1212,6 +1193,7 @@ def acquire_token_silent(
1212
1193
authority = None , # See get_authorization_request_url()
1213
1194
force_refresh = False , # type: Optional[boolean]
1214
1195
claims_challenge = None ,
1196
+ auth_scheme = None ,
1215
1197
** kwargs ):
1216
1198
"""Acquire an access token for given account, without user interaction.
1217
1199
@@ -1232,7 +1214,7 @@ def acquire_token_silent(
1232
1214
return None # A backward-compatible NO-OP to drop the account=None usage
1233
1215
result = _clean_up (self ._acquire_token_silent_with_error (
1234
1216
scopes , account , authority = authority , force_refresh = force_refresh ,
1235
- claims_challenge = claims_challenge , ** kwargs ))
1217
+ claims_challenge = claims_challenge , auth_scheme = auth_scheme , ** kwargs ))
1236
1218
return result if result and "error" not in result else None
1237
1219
1238
1220
def acquire_token_silent_with_error (
@@ -1242,6 +1224,7 @@ def acquire_token_silent_with_error(
1242
1224
authority = None , # See get_authorization_request_url()
1243
1225
force_refresh = False , # type: Optional[boolean]
1244
1226
claims_challenge = None ,
1227
+ auth_scheme = None ,
1245
1228
** kwargs ):
1246
1229
"""Acquire an access token for given account, without user interaction.
1247
1230
@@ -1268,6 +1251,12 @@ def acquire_token_silent_with_error(
1268
1251
in the form of a claims_challenge directive in the www-authenticate header to be
1269
1252
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
1270
1253
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
+
1271
1260
:return:
1272
1261
- A dict containing no "error" key,
1273
1262
and typically contains an "access_token" key,
@@ -1279,7 +1268,7 @@ def acquire_token_silent_with_error(
1279
1268
return None # A backward-compatible NO-OP to drop the account=None usage
1280
1269
return _clean_up (self ._acquire_token_silent_with_error (
1281
1270
scopes , account , authority = authority , force_refresh = force_refresh ,
1282
- claims_challenge = claims_challenge , ** kwargs ))
1271
+ claims_challenge = claims_challenge , auth_scheme = auth_scheme , ** kwargs ))
1283
1272
1284
1273
def _acquire_token_silent_with_error (
1285
1274
self ,
@@ -1288,6 +1277,7 @@ def _acquire_token_silent_with_error(
1288
1277
authority = None , # See get_authorization_request_url()
1289
1278
force_refresh = False , # type: Optional[boolean]
1290
1279
claims_challenge = None ,
1280
+ auth_scheme = None ,
1291
1281
** kwargs ):
1292
1282
assert isinstance (scopes , list ), "Invalid parameter type"
1293
1283
self ._validate_ssh_cert_input_data (kwargs .get ("data" , {}))
@@ -1303,6 +1293,7 @@ def _acquire_token_silent_with_error(
1303
1293
scopes , account , self .authority , force_refresh = force_refresh ,
1304
1294
claims_challenge = claims_challenge ,
1305
1295
correlation_id = correlation_id ,
1296
+ auth_scheme = auth_scheme ,
1306
1297
** kwargs )
1307
1298
if result and "error" not in result :
1308
1299
return result
@@ -1325,6 +1316,7 @@ def _acquire_token_silent_with_error(
1325
1316
scopes , account , the_authority , force_refresh = force_refresh ,
1326
1317
claims_challenge = claims_challenge ,
1327
1318
correlation_id = correlation_id ,
1319
+ auth_scheme = auth_scheme ,
1328
1320
** kwargs )
1329
1321
if result :
1330
1322
if "error" not in result :
@@ -1349,12 +1341,13 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
1349
1341
claims_challenge = None ,
1350
1342
correlation_id = None ,
1351
1343
http_exceptions = None ,
1344
+ auth_scheme = None ,
1352
1345
** kwargs ):
1353
1346
# This internal method has two calling patterns:
1354
1347
# it accepts a non-empty account to find token for a user,
1355
1348
# and accepts account=None to find a token for the current app.
1356
1349
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
1358
1351
query = {
1359
1352
"client_id" : self .client_id ,
1360
1353
"environment" : authority .instance ,
@@ -1397,6 +1390,8 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
1397
1390
try :
1398
1391
data = kwargs .get ("data" , {})
1399
1392
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" )
1400
1395
return self ._acquire_token_by_cloud_shell (scopes , data = data )
1401
1396
1402
1397
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(
1412
1407
claims = _merge_claims_challenge_and_capabilities (
1413
1408
self ._client_capabilities , claims_challenge ),
1414
1409
correlation_id = correlation_id ,
1410
+ auth_scheme = auth_scheme ,
1415
1411
** data )
1416
1412
if response : # Broker provides a decisive outcome
1417
1413
account_was_established_by_broker = account .get (
@@ -1420,6 +1416,8 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
1420
1416
if account_was_established_by_broker or broker_attempt_succeeded_just_now :
1421
1417
return self ._process_broker_response (response , scopes , data )
1422
1418
1419
+ if auth_scheme :
1420
+ raise ValueError (self ._AUTH_SCHEME_UNSUPPORTED )
1423
1421
if account :
1424
1422
result = self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
1425
1423
authority , self ._decorate_scope (scopes ), account ,
@@ -1615,7 +1613,11 @@ def acquire_token_by_refresh_token(self, refresh_token, scopes, **kwargs):
1615
1613
return response
1616
1614
1617
1615
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 ):
1619
1621
"""Gets a token for a given resource via user credentials.
1620
1622
1621
1623
See this page for constraints of Username Password Flow.
@@ -1631,6 +1633,12 @@ def acquire_token_by_username_password(
1631
1633
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
1632
1634
It is a string of a JSON object which contains lists of claims being requested from these locations.
1633
1635
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
+
1634
1642
:return: A dict representing the json response from AAD:
1635
1643
1636
1644
- A successful response would contain "access_token" key,
@@ -1650,9 +1658,12 @@ def acquire_token_by_username_password(
1650
1658
self .authority ._is_known_to_developer
1651
1659
or self ._instance_discovery is False ) else None ,
1652
1660
claims = claims ,
1661
+ auth_scheme = auth_scheme ,
1653
1662
)
1654
1663
return self ._process_broker_response (response , scopes , kwargs .get ("data" , {}))
1655
1664
1665
+ if auth_scheme :
1666
+ raise ValueError (self ._AUTH_SCHEME_UNSUPPORTED )
1656
1667
scopes = self ._decorate_scope (scopes )
1657
1668
telemetry_context = self ._build_telemetry_context (
1658
1669
self .ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID )
@@ -1795,6 +1806,7 @@ def acquire_token_interactive(
1795
1806
max_age = None ,
1796
1807
parent_window_handle = None ,
1797
1808
on_before_launching_ui = None ,
1809
+ auth_scheme = None ,
1798
1810
** kwargs ):
1799
1811
"""Acquire token interactively i.e. via a local browser.
1800
1812
@@ -1870,6 +1882,12 @@ def acquire_token_interactive(
1870
1882
1871
1883
New in version 1.20.0.
1872
1884
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
+
1873
1891
:return:
1874
1892
- A dict containing no "error" key,
1875
1893
and typically contains an "access_token" key.
@@ -1914,12 +1932,15 @@ def acquire_token_interactive(
1914
1932
claims ,
1915
1933
data ,
1916
1934
on_before_launching_ui ,
1935
+ auth_scheme ,
1917
1936
prompt = prompt ,
1918
1937
login_hint = login_hint ,
1919
1938
max_age = max_age ,
1920
1939
)
1921
1940
return self ._process_broker_response (response , scopes , data )
1922
1941
1942
+ if auth_scheme :
1943
+ raise ValueError ("auth_scheme is currently only available from broker" )
1923
1944
on_before_launching_ui (ui = "browser" )
1924
1945
telemetry_context = self ._build_telemetry_context (
1925
1946
self .ACQUIRE_TOKEN_INTERACTIVE )
@@ -1954,6 +1975,7 @@ def _acquire_token_interactive_via_broker(
1954
1975
claims , # type: str
1955
1976
data , # type: dict
1956
1977
on_before_launching_ui , # type: callable
1978
+ auth_scheme , # type: object
1957
1979
prompt = None ,
1958
1980
login_hint = None , # type: Optional[str]
1959
1981
max_age = None ,
@@ -1977,6 +1999,7 @@ def _acquire_token_interactive_via_broker(
1977
1999
accounts [0 ]["local_account_id" ],
1978
2000
scopes ,
1979
2001
claims = claims ,
2002
+ auth_scheme = auth_scheme ,
1980
2003
** data )
1981
2004
if response and "error" not in response :
1982
2005
return response
@@ -1989,6 +2012,7 @@ def _acquire_token_interactive_via_broker(
1989
2012
claims = claims ,
1990
2013
max_age = max_age ,
1991
2014
enable_msa_pt = enable_msa_passthrough ,
2015
+ auth_scheme = auth_scheme ,
1992
2016
** data )
1993
2017
is_wrong_account = bool (
1994
2018
# _signin_silently() only gets tokens for default account,
@@ -2029,6 +2053,7 @@ def _acquire_token_interactive_via_broker(
2029
2053
claims = claims ,
2030
2054
max_age = max_age ,
2031
2055
enable_msa_pt = enable_msa_passthrough ,
2056
+ auth_scheme = auth_scheme ,
2032
2057
** data )
2033
2058
2034
2059
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
2176
2201
"""
2177
2202
telemetry_context = self ._build_telemetry_context (
2178
2203
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)
2181
2205
response = _clean_up (self .client .obtain_token_by_assertion ( # bases on assertion RFC 7521
2182
2206
user_assertion ,
2183
2207
self .client .GRANT_TYPE_JWT , # IDTs and AAD ATs are all JWTs
0 commit comments