Skip to content

Commit 1b316e3

Browse files
authored
Merge pull request #577 from AzureAD/silent-adjustment
Remove acquire_token_silent(..., account=None) usage in a backward-compatible way
2 parents e1e3d1c + 2288b77 commit 1b316e3

File tree

5 files changed

+99
-62
lines changed

5 files changed

+99
-62
lines changed

msal/application.py

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,32 +1209,24 @@ def acquire_token_silent(
12091209
**kwargs):
12101210
"""Acquire an access token for given account, without user interaction.
12111211
1212-
It is done either by finding a valid access token from cache,
1213-
or by finding a valid refresh token from cache and then automatically
1214-
use it to redeem a new access token.
1215-
1212+
It has same parameters as the :func:`~acquire_token_silent_with_error`.
1213+
The difference is the behavior of the return value.
12161214
This method will combine the cache empty and refresh error
12171215
into one return value, `None`.
12181216
If your app does not care about the exact token refresh error during
12191217
token cache look-up, then this method is easier and recommended.
12201218
1221-
Internally, this method calls :func:`~acquire_token_silent_with_error`.
1222-
1223-
:param claims_challenge:
1224-
The claims_challenge parameter requests specific claims requested by the resource provider
1225-
in the form of a claims_challenge directive in the www-authenticate header to be
1226-
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
1227-
It is a string of a JSON object which contains lists of claims being requested from these locations.
1228-
12291219
:return:
12301220
- A dict containing no "error" key,
12311221
and typically contains an "access_token" key,
12321222
if cache lookup succeeded.
12331223
- None when cache lookup does not yield a token.
12341224
"""
1235-
result = self.acquire_token_silent_with_error(
1225+
if not account:
1226+
return None # A backward-compatible NO-OP to drop the account=None usage
1227+
result = _clean_up(self._acquire_token_silent_with_error(
12361228
scopes, account, authority=authority, force_refresh=force_refresh,
1237-
claims_challenge=claims_challenge, **kwargs)
1229+
claims_challenge=claims_challenge, **kwargs))
12381230
return result if result and "error" not in result else None
12391231

12401232
def acquire_token_silent_with_error(
@@ -1258,9 +1250,10 @@ def acquire_token_silent_with_error(
12581250
12591251
:param list[str] scopes: (Required)
12601252
Scopes requested to access a protected API (a resource).
1261-
:param account:
1262-
one of the account object returned by :func:`~get_accounts`,
1263-
or use None when you want to find an access token for this client.
1253+
:param account: (Required)
1254+
One of the account object returned by :func:`~get_accounts`.
1255+
Starting from MSAL Python 1.23,
1256+
a ``None`` input will become a NO-OP and always return ``None``.
12641257
:param force_refresh:
12651258
If True, it will skip Access Token look-up,
12661259
and try to find a Refresh Token to obtain a new Access Token.
@@ -1276,6 +1269,20 @@ def acquire_token_silent_with_error(
12761269
- None when there is simply no token in the cache.
12771270
- A dict containing an "error" key, when token refresh failed.
12781271
"""
1272+
if not account:
1273+
return None # A backward-compatible NO-OP to drop the account=None usage
1274+
return _clean_up(self._acquire_token_silent_with_error(
1275+
scopes, account, authority=authority, force_refresh=force_refresh,
1276+
claims_challenge=claims_challenge, **kwargs))
1277+
1278+
def _acquire_token_silent_with_error(
1279+
self,
1280+
scopes, # type: List[str]
1281+
account, # type: Optional[Account]
1282+
authority=None, # See get_authorization_request_url()
1283+
force_refresh=False, # type: Optional[boolean]
1284+
claims_challenge=None,
1285+
**kwargs):
12791286
assert isinstance(scopes, list), "Invalid parameter type"
12801287
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
12811288
correlation_id = msal.telemetry._get_new_correlation_id()
@@ -1335,7 +1342,11 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
13351342
force_refresh=False, # type: Optional[boolean]
13361343
claims_challenge=None,
13371344
correlation_id=None,
1345+
http_exceptions=None,
13381346
**kwargs):
1347+
# This internal method has two calling patterns:
1348+
# it accepts a non-empty account to find token for a user,
1349+
# and accepts account=None to find a token for the current app.
13391350
access_token_from_cache = None
13401351
if not (force_refresh or claims_challenge): # Bypass AT when desired or using claims
13411352
query={
@@ -1372,6 +1383,10 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
13721383
else:
13731384
refresh_reason = msal.telemetry.FORCE_REFRESH # TODO: It could also mean claims_challenge
13741385
assert refresh_reason, "It should have been established at this point"
1386+
if not http_exceptions: # It can be a tuple of exceptions
1387+
# The exact HTTP exceptions are transportation-layer dependent
1388+
from requests.exceptions import RequestException # Lazy load
1389+
http_exceptions = (RequestException,)
13751390
try:
13761391
data = kwargs.get("data", {})
13771392
if account and account.get("authority_type") == _AUTHORITY_TYPE_CLOUDSHELL:
@@ -1391,14 +1406,19 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
13911406
if response: # The broker provided a decisive outcome, so we use it
13921407
return self._process_broker_response(response, scopes, data)
13931408

1394-
result = _clean_up(self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
1395-
authority, self._decorate_scope(scopes), account,
1396-
refresh_reason=refresh_reason, claims_challenge=claims_challenge,
1397-
correlation_id=correlation_id,
1398-
**kwargs))
1409+
if account:
1410+
result = self._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
1411+
authority, self._decorate_scope(scopes), account,
1412+
refresh_reason=refresh_reason, claims_challenge=claims_challenge,
1413+
correlation_id=correlation_id,
1414+
**kwargs)
1415+
else: # The caller is acquire_token_for_client()
1416+
result = self._acquire_token_for_client(
1417+
scopes, refresh_reason, claims_challenge=claims_challenge,
1418+
**kwargs)
13991419
if (result and "error" not in result) or (not access_token_from_cache):
14001420
return result
1401-
except: # The exact HTTP exception is transportation-layer dependent
1421+
except http_exceptions:
14021422
# Typically network error. Potential AAD outage?
14031423
if not access_token_from_cache: # It means there is no fall back option
14041424
raise # We choose to bubble up the exception
@@ -2007,6 +2027,9 @@ class ConfidentialClientApplication(ClientApplication): # server-side web app
20072027
def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
20082028
"""Acquires token for the current confidential client, not for an end user.
20092029
2030+
Since MSAL Python 1.23, it will automatically look for token from cache,
2031+
and only send request to Identity Provider when cache misses.
2032+
20102033
:param list[str] scopes: (Required)
20112034
Scopes requested to access a protected API (a resource).
20122035
:param claims_challenge:
@@ -2020,24 +2043,37 @@ def acquire_token_for_client(self, scopes, claims_challenge=None, **kwargs):
20202043
- A successful response would contain "access_token" key,
20212044
- an error response would contain "error" and usually "error_description".
20222045
"""
2023-
# TBD: force_refresh behavior
2046+
if kwargs.get("force_refresh"):
2047+
raise ValueError( # We choose to disallow force_refresh
2048+
"Historically, this method does not support force_refresh behavior. "
2049+
)
2050+
return _clean_up(self._acquire_token_silent_with_error(
2051+
scopes, None, claims_challenge=claims_challenge, **kwargs))
2052+
2053+
def _acquire_token_for_client(
2054+
self,
2055+
scopes,
2056+
refresh_reason,
2057+
claims_challenge=None,
2058+
**kwargs
2059+
):
20242060
if self.authority.tenant.lower() in ["common", "organizations"]:
20252061
warnings.warn(
20262062
"Using /common or /organizations authority "
20272063
"in acquire_token_for_client() is unreliable. "
20282064
"Please use a specific tenant instead.", DeprecationWarning)
20292065
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
20302066
telemetry_context = self._build_telemetry_context(
2031-
self.ACQUIRE_TOKEN_FOR_CLIENT_ID)
2067+
self.ACQUIRE_TOKEN_FOR_CLIENT_ID, refresh_reason=refresh_reason)
20322068
client = self._regional_client or self.client
2033-
response = _clean_up(client.obtain_token_for_client(
2069+
response = client.obtain_token_for_client(
20342070
scope=scopes, # This grant flow requires no scope decoration
20352071
headers=telemetry_context.generate_headers(),
20362072
data=dict(
20372073
kwargs.pop("data", {}),
20382074
claims=_merge_claims_challenge_and_capabilities(
20392075
self._client_capabilities, claims_challenge)),
2040-
**kwargs))
2076+
**kwargs)
20412077
telemetry_context.update_telemetry(response)
20422078
return response
20432079

sample/confidential_client_certificate_sample.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,9 @@
5151
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
5252
)
5353

54-
# The pattern to acquire a token looks like this.
55-
result = None
56-
57-
# Firstly, looks up a token from cache
58-
# Since we are looking for token for the current app, NOT for an end user,
59-
# notice we give account parameter as None.
60-
result = app.acquire_token_silent(config["scope"], account=None)
61-
62-
if not result:
63-
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
64-
result = app.acquire_token_for_client(scopes=config["scope"])
54+
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
55+
# a token from cache, and fall back to acquire a fresh token when needed.
56+
result = app.acquire_token_for_client(scopes=config["scope"])
6557

6658
if "access_token" in result:
6759
# Calling graph using the access token

sample/confidential_client_secret_sample.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,9 @@
5050
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
5151
)
5252

53-
# The pattern to acquire a token looks like this.
54-
result = None
55-
56-
# Firstly, looks up a token from cache
57-
# Since we are looking for token for the current app, NOT for an end user,
58-
# notice we give account parameter as None.
59-
result = app.acquire_token_silent(config["scope"], account=None)
60-
61-
if not result:
62-
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
63-
result = app.acquire_token_for_client(scopes=config["scope"])
53+
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
54+
# a token from cache, and fall back to acquire a fresh token when needed.
55+
result = app.acquire_token_for_client(scopes=config["scope"])
6456

6557
if "access_token" in result:
6658
# Calling graph using the access token

tests/test_application.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ def test_aging_token_and_unavailable_aad_should_return_old_token(self):
382382
old_at = "old AT"
383383
self.populate_cache(access_token=old_at, expires_in=3599, refresh_in=-1)
384384
def mock_post(url, headers=None, *args, **kwargs):
385-
self.assertEqual("4|84,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
386-
return MinimalResponse(status_code=400, text=json.dumps({"error": error}))
385+
self.assertEqual("4|84,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
386+
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
387387
result = self.app.acquire_token_silent(['s1'], self.account, post=mock_post)
388388
self.assertEqual(old_at, result.get("access_token"))
389389

@@ -549,12 +549,31 @@ def setUpClass(cls): # Initialization at runtime, not interpret-time
549549
authority="https://login.microsoftonline.com/common")
550550

551551
def test_acquire_token_for_client(self):
552-
at = "this is an access token"
553552
def mock_post(url, headers=None, *args, **kwargs):
554-
self.assertEqual("4|730,0|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
555-
return MinimalResponse(status_code=200, text=json.dumps({"access_token": at}))
553+
self.assertEqual("4|730,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
554+
return MinimalResponse(status_code=200, text=json.dumps({
555+
"access_token": "AT 1",
556+
"expires_in": 0,
557+
}))
556558
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
557-
self.assertEqual(at, result.get("access_token"))
559+
self.assertEqual("AT 1", result.get("access_token"), "Shall get a new token")
560+
561+
def mock_post(url, headers=None, *args, **kwargs):
562+
self.assertEqual("4|730,3|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
563+
return MinimalResponse(status_code=200, text=json.dumps({
564+
"access_token": "AT 2",
565+
"expires_in": 3600,
566+
"refresh_in": -100, # A hack to make sure it will attempt refresh
567+
}))
568+
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
569+
self.assertEqual("AT 2", result.get("access_token"), "Shall get a new token")
570+
571+
def mock_post(url, headers=None, *args, **kwargs):
572+
# 1/0 # TODO: Make sure this was called
573+
self.assertEqual("4|730,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
574+
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
575+
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
576+
self.assertEqual("AT 2", result.get("access_token"), "Shall get aging token")
558577

559578
def test_acquire_token_on_behalf_of(self):
560579
at = "this is an access token"

tests/test_e2e.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,15 @@ def assertCacheWorksForApp(self, result_from_wire, scope):
146146
json.dumps(self.app.token_cache._cache, indent=4),
147147
json.dumps(result_from_wire.get("id_token_claims"), indent=4),
148148
)
149-
# Going to test acquire_token_silent(...) to locate an AT from cache
150-
result_from_cache = self.app.acquire_token_silent(scope, account=None)
149+
self.assertIsNone(
150+
self.app.acquire_token_silent(scope, account=None),
151+
"acquire_token_silent(..., account=None) shall always return None")
152+
# Going to test acquire_token_for_client(...) to locate an AT from cache
153+
result_from_cache = self.app.acquire_token_for_client(scope)
151154
self.assertIsNotNone(result_from_cache)
152155
self.assertEqual(
153156
result_from_wire['access_token'], result_from_cache['access_token'],
154157
"We should get a cached AT")
155-
self.app.acquire_token_silent(
156-
# Result will typically be None, because client credential grant returns no RT.
157-
# But we care more on this call should succeed without exception.
158-
scope, account=None,
159-
force_refresh=True) # Mimic the AT already expires
160158

161159
@classmethod
162160
def _build_app(cls,

0 commit comments

Comments
 (0)