Skip to content

Commit b282045

Browse files
committed
Refactor to provide IDT, and demo how to do cache
1 parent a756d0a commit b282045

File tree

2 files changed

+32
-13
lines changed

2 files changed

+32
-13
lines changed

msal/application.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -656,9 +656,12 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
656656
return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
657657
user_assertion,
658658
self.client.GRANT_TYPE_JWT, # IDTs and AAD ATs are all JWTs
659-
scope=scopes, # Without decorate_scope(...), it still gets an AT.
660-
# As of 2019, AAD would even issue RT, and ClientInfo i.e. account.
661-
# No IDT will be issued. OBO app probably does not need one anyway.
659+
scope=decorate_scope(scopes, self.client_id), # Decoration is used for:
660+
# 1. Explicitly requesting an RT, without relying on AAD default
661+
# behavior, even though it currently still issues an RT.
662+
# 2. Requesting an IDT (which would otherwise be unavailable)
663+
# so that the calling app could use id_token_claims to implement
664+
# their own cache mapping, which is likely needed in web apps.
662665
data=dict(kwargs.pop("data", {}), requested_token_use="on_behalf_of"),
663666
**kwargs)
664667

tests/test_e2e.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -330,30 +330,46 @@ def test_adfs2019_fed_user(self):
330330
@unittest.skipUnless(
331331
os.getenv("OBO_CLIENT_SECRET"),
332332
"Need OBO_CLIENT_SECRET from https://buildautomation.vault.azure.net/secrets/IdentityDivisionDotNetOBOServiceSecret")
333-
def test_acquire_token_obo(self): # It hardcodes many pre-defined resources
333+
def test_acquire_token_obo(self):
334+
# Some hardcoded, pre-defined settings
334335
obo_client_id = "23c64cd8-21e4-41dd-9756-ab9e2c23f58c"
335-
obo_scopes = ["https://graph.microsoft.com/User.Read"]
336+
downstream_scopes = ["https://graph.microsoft.com/User.Read"]
336337
config = get_lab_user(isFederated=False)
338+
339+
# 1. An app obtains a token representing a user, for our mid-tier service
337340
pca = msal.PublicClientApplication(
338341
"be9b0186-7dfd-448a-a944-f771029105bf", authority=config.get("authority"))
339342
pca_result = pca.acquire_token_by_username_password(
340343
config["username"],
341344
self.get_lab_user_secret(config["lab"]["labname"]),
342-
scopes=["%s/access_as_user" % obo_client_id], # Need setup beforehand
345+
scopes=[ # The OBO app's scope. Yours might be different.
346+
"%s/access_as_user" % obo_client_id],
343347
)
344-
self.assertNotEqual(None, pca_result.get("access_token"), "PCA should work")
348+
self.assertIsNotNone(pca_result.get("access_token"), "PCA should work")
345349

350+
# 2. Our mid-tier service uses OBO to obtain a token for downstream service
346351
cca = msal.ConfidentialClientApplication(
347352
obo_client_id,
348353
client_credential=os.getenv("OBO_CLIENT_SECRET"),
349-
authority=config.get("authority"))
354+
authority=config.get("authority"),
355+
# token_cache= ..., # Default token cache is all-tokens-store-in-memory.
356+
# That's fine if OBO app uses short-lived msal instance per session.
357+
# Otherwise, the OBO app need to implement a one-cache-per-user setup.
358+
)
350359
cca_result = cca.acquire_token_on_behalf_of(
351-
pca_result['access_token'], obo_scopes)
360+
pca_result['access_token'], downstream_scopes)
352361
self.assertNotEqual(None, cca_result.get("access_token"), str(cca_result))
353362

354-
# Cache would also work, with the one-cache-per-user caveat.
355-
if len(cca.get_accounts()) == 1:
356-
account = cca.get_accounts()[0] # This test involves only 1 account
357-
result = cca.acquire_token_silent(obo_scopes, account)
363+
# 3. Now the OBO app can simply store downstream token(s) in same session.
364+
# Alternatively, if you want to persist the downstream AT, and possibly
365+
# the RT (if any) for prolonged access even after your own AT expires,
366+
# now it is the time to persist current cache state for current user.
367+
# Assuming you already did that (which is not shown in this test case),
368+
# the following part shows one of the ways to obtain an AT from cache.
369+
username = cca_result.get("id_token_claims", {}).get("preferred_username")
370+
self.assertEqual(config["username"], username)
371+
if username: # A precaution so that we won't use other user's token
372+
account = cca.get_accounts(username=username)[0]
373+
result = cca.acquire_token_silent(downstream_scopes, account)
358374
self.assertEqual(cca_result["access_token"], result["access_token"])
359375

0 commit comments

Comments
 (0)