Skip to content

Commit edc1c58

Browse files
committed
Add a new acquire_token_silent_with_error()
1 parent 2a3e1c4 commit edc1c58

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

msal/application.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,40 @@ def acquire_token_silent(
431431
**kwargs):
432432
"""Acquire an access token for given account, without user interaction.
433433
434+
It behaves same as :func:`~acquire_token_silent_with_error`,
435+
except that this method will combine the cache empty and refresh error
436+
into one return value, `None`.
437+
If your app does not need to care the exact token refresh error during
438+
token cache look-up, then this method is easier to use.
439+
440+
Internally, this method calls :func:`~acquire_token_silent_with_error`.
441+
442+
:return:
443+
- A dict containing no "error" key,
444+
and typically contains an "access_token" key,
445+
if cache lookup succeeded.
446+
- None when cache lookup does not yield a token.
447+
"""
448+
result = self.acquire_token_silent_with_error(
449+
scopes, account, authority, force_refresh, **kwargs)
450+
return result if result and "error" not in result else None
451+
452+
def acquire_token_silent_with_error(
453+
self,
454+
scopes, # type: List[str]
455+
account, # type: Optional[Account]
456+
authority=None, # See get_authorization_request_url()
457+
force_refresh=False, # type: Optional[boolean]
458+
**kwargs):
459+
"""Acquire an access token for given account, without user interaction.
460+
434461
It is done either by finding a valid access token from cache,
435462
or by finding a valid refresh token from cache and then automatically
436463
use it to redeem a new access token.
437464
465+
Unlike :func:`~acquire_token_silent`,
466+
error happened during token refresh would also be returned.
467+
438468
:param list[str] scopes: (Required)
439469
Scopes requested to access a protected API (a resource).
440470
:param account:
@@ -444,8 +474,11 @@ def acquire_token_silent(
444474
If True, it will skip Access Token look-up,
445475
and try to find a Refresh Token to obtain a new Access Token.
446476
:return:
447-
- A dict containing "access_token" key, when cache lookup succeeds.
448-
- None when cache lookup does not yield anything.
477+
- A dict containing no "error" key,
478+
and typically contains an "access_token" key,
479+
if cache lookup succeeded.
480+
- None when there is simply no token in the cache.
481+
- A dict containing an "error" key, when token refresh failed.
449482
"""
450483
assert isinstance(scopes, list), "Invalid parameter type"
451484
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
@@ -460,8 +493,9 @@ def acquire_token_silent(
460493
scopes, account, self.authority, force_refresh=force_refresh,
461494
correlation_id=correlation_id,
462495
**kwargs)
463-
if result:
496+
if result and "error" not in result:
464497
return result
498+
final_result = result
465499
for alias in self._get_authority_aliases(self.authority.instance):
466500
the_authority = Authority(
467501
"https://" + alias + "/" + self.authority.tenant,
@@ -472,7 +506,10 @@ def acquire_token_silent(
472506
correlation_id=correlation_id,
473507
**kwargs)
474508
if result:
475-
return result
509+
if "error" not in result:
510+
return result
511+
final_result = result
512+
return final_result
476513

477514
def _acquire_token_silent_from_cache_and_possibly_refresh_it(
478515
self,
@@ -533,13 +570,13 @@ def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family(
533570
# https://msazure.visualstudio.com/One/_git/ESTS-Docs/pullrequest/1138595
534571
"client_mismatch" in response.get("error_additional_info", []),
535572
**kwargs)
536-
if at:
573+
if at and "error" not in at:
537574
return at
538575
if app_metadata.get("family_id"): # Meaning this app belongs to this family
539576
at = self._acquire_token_silent_by_finding_specific_refresh_token(
540577
authority, scopes, dict(query, family_id=app_metadata["family_id"]),
541578
**kwargs)
542-
if at:
579+
if at and "error" not in at:
543580
return at
544581
# Either this app is an orphan, so we will naturally use its own RT;
545582
# or all attempts above have failed, so we fall back to non-foci behavior.
@@ -562,6 +599,8 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
562599
query=query)
563600
logger.debug("Found %d RTs matching %s", len(matches), query)
564601
client = self._build_client(self.client_credential, authority)
602+
603+
response = None # A distinguishable value to mean cache is empty
565604
for entry in matches:
566605
logger.debug("Cache attempts an RT")
567606
response = client.obtain_token_by_refresh_token(
@@ -582,6 +621,7 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
582621
))
583622
if break_condition(response):
584623
break
624+
return response # Returns the latest error (if any), or just None
585625

586626
def _validate_ssh_cert_input_data(self, data):
587627
if data.get("token_type") == "ssh-cert":

tests/test_application.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,49 @@ def test_extract_multiple_tag_enclosed_certs(self):
4646
self.assertEqual(["my_cert1", "my_cert2"], extract_certs(pem))
4747

4848

49+
class TestClientApplicationAcquireTokenSilentErrorBehaviors(unittest.TestCase):
50+
51+
def setUp(self):
52+
self.authority_url = "https://login.microsoftonline.com/common"
53+
self.authority = msal.authority.Authority(self.authority_url)
54+
self.scopes = ["s1", "s2"]
55+
self.uid = "my_uid"
56+
self.utid = "my_utid"
57+
self.account = {"home_account_id": "{}.{}".format(self.uid, self.utid)}
58+
self.rt = "this is a rt"
59+
self.cache = msal.SerializableTokenCache()
60+
self.client_id = "my_app"
61+
self.cache.add({ # Pre-populate the cache
62+
"client_id": self.client_id,
63+
"scope": self.scopes,
64+
"token_endpoint": "{}/oauth2/v2.0/token".format(self.authority_url),
65+
"response": TokenCacheTestCase.build_response(
66+
access_token="an expired AT to trigger refresh", expires_in=-99,
67+
uid=self.uid, utid=self.utid, refresh_token=self.rt),
68+
}) # The add(...) helper populates correct home_account_id for future searching
69+
self.app = ClientApplication(
70+
self.client_id, authority=self.authority_url, token_cache=self.cache)
71+
72+
def test_cache_empty_will_be_returned_as_None(self):
73+
self.assertEqual(
74+
None, self.app.acquire_token_silent(['cache_miss'], self.account))
75+
self.assertEqual(
76+
None, self.app.acquire_token_silent_with_error(['cache_miss'], self.account))
77+
78+
def test_acquire_token_silent_with_error_will_return_error(self):
79+
error_response = {"error": "invalid_grant", "error_description": "xyz"}
80+
def tester(url, **kwargs):
81+
return Mock(status_code=400, json=Mock(return_value=error_response))
82+
self.assertEqual(error_response, self.app.acquire_token_silent_with_error(
83+
self.scopes, self.account, post=tester))
84+
85+
def test_acquire_token_silent_will_suppress_error(self):
86+
error_response = {"error": "invalid_grant", "error_description": "xyz"}
87+
def tester(url, **kwargs):
88+
return Mock(status_code=400, json=Mock(return_value=error_response))
89+
self.assertEqual(None, self.app.acquire_token_silent(
90+
self.scopes, self.account, post=tester))
91+
4992
class TestClientApplicationAcquireTokenSilentFociBehaviors(unittest.TestCase):
5093

5194
def setUp(self):

0 commit comments

Comments
 (0)