Skip to content

Commit a756d0a

Browse files
committed
Rework the previous work stopped at 3-years ago
1 parent 13868ed commit a756d0a

File tree

3 files changed

+53
-23
lines changed

3 files changed

+53
-23
lines changed

msal/application.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ def _acquire_token_by_username_password_federated(
624624
class ConfidentialClientApplication(ClientApplication): # server-side web app
625625

626626
def acquire_token_for_client(self, scopes, **kwargs):
627-
"""Acquires token from the service for the confidential client.
627+
"""Acquires token for the current confidential client, not for an end user.
628628
629629
:param list[str] scopes: (Required)
630630
Scopes requested to access a protected API (a resource).
@@ -639,17 +639,26 @@ def acquire_token_for_client(self, scopes, **kwargs):
639639
scope=scopes, # This grant flow requires no scope decoration
640640
**kwargs)
641641

642-
def acquire_token_on_behalf_of(
643-
self, user_assertion, scope, authority=None, policy=''):
644-
the_authority = Authority(authority) if authority else self.authority
645-
return oauth2.Client(
646-
self.client_id, token_endpoint=the_authority.token_endpoint,
647-
default_body=self._build_auth_parameters(
648-
self.client_credential, the_authority.token_endpoint,
649-
self.client_id)
650-
)._get_token( # TODO: Avoid using internal methods
651-
"urn:ietf:params:oauth:grant-type:jwt-bearer",
652-
assertion=user_assertion, requested_token_use='on_behalf_of',
653-
scope=scope, # This grant flow requires no scope decoration???
654-
query={'p': policy} if policy else None)
642+
def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
643+
"""Acquires token using on-behalf-of (OBO) flow.
644+
645+
The current app is a middle-tier service which already receives a token
646+
representing an end user.
647+
The current app can use such token (a.k.a. a user assertion) to request
648+
another token to access downstream service, on behalf of that user.
649+
650+
The current middle-tier app has no user interaction to obtain consent.
651+
See how to gain consent upfront for your middle-tier app from this article.
652+
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#gaining-consent-for-the-middle-tier-application
653+
"""
654+
# The implementation is NOT based on Token Exchange
655+
# https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16
656+
return self.client.obtain_token_by_assertion( # bases on assertion RFC 7521
657+
user_assertion,
658+
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.
662+
data=dict(kwargs.pop("data", {}), requested_token_use="on_behalf_of"),
663+
**kwargs)
655664

tests/test_application.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,3 @@ def test_acquire_token_silent(self):
194194
self.assertNotEqual(None, at)
195195
self.assertEqual(self.access_token, at.get('access_token'))
196196

197-
def test_acquire_token_obo(self):
198-
token = self.app.acquire_token_on_behalf_of(
199-
self.token['access_token'], self.scope2)
200-
error_description = token.get('error_description', "")
201-
if 'grant is not supported by this API version' in error_description:
202-
raise unittest.SkipTest(
203-
"OBO is not yet supported by service: %s" % error_description)
204-
self.assertEqual(error_description, "")
205-

tests/test_e2e.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,33 @@ def test_adfs2019_fed_user(self):
327327
self._test_username_password(
328328
password=self.get_lab_user_secret(config["lab"]["labname"]), **config)
329329

330+
@unittest.skipUnless(
331+
os.getenv("OBO_CLIENT_SECRET"),
332+
"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
334+
obo_client_id = "23c64cd8-21e4-41dd-9756-ab9e2c23f58c"
335+
obo_scopes = ["https://graph.microsoft.com/User.Read"]
336+
config = get_lab_user(isFederated=False)
337+
pca = msal.PublicClientApplication(
338+
"be9b0186-7dfd-448a-a944-f771029105bf", authority=config.get("authority"))
339+
pca_result = pca.acquire_token_by_username_password(
340+
config["username"],
341+
self.get_lab_user_secret(config["lab"]["labname"]),
342+
scopes=["%s/access_as_user" % obo_client_id], # Need setup beforehand
343+
)
344+
self.assertNotEqual(None, pca_result.get("access_token"), "PCA should work")
345+
346+
cca = msal.ConfidentialClientApplication(
347+
obo_client_id,
348+
client_credential=os.getenv("OBO_CLIENT_SECRET"),
349+
authority=config.get("authority"))
350+
cca_result = cca.acquire_token_on_behalf_of(
351+
pca_result['access_token'], obo_scopes)
352+
self.assertNotEqual(None, cca_result.get("access_token"), str(cca_result))
353+
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)
358+
self.assertEqual(cca_result["access_token"], result["access_token"])
359+

0 commit comments

Comments
 (0)