Skip to content

Commit 4cfbba7

Browse files
authored
Merge pull request #260 from AzureAD/acquire-token-interactive
Acquire token interactive using system browser
2 parents 49ce6c5 + de66698 commit 4cfbba7

File tree

2 files changed

+123
-1
lines changed

2 files changed

+123
-1
lines changed

msal/application.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class ClientApplication(object):
107107
ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID = "622"
108108
ACQUIRE_TOKEN_FOR_CLIENT_ID = "730"
109109
ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID = "832"
110+
ACQUIRE_TOKEN_INTERACTIVE = "169"
110111
GET_ACCOUNTS_ID = "902"
111112
REMOVE_ACCOUNT_ID = "903"
112113

@@ -318,7 +319,6 @@ def initiate_auth_code_flow(
318319
319320
:param list scope:
320321
It is a list of case-sensitive strings.
321-
Some ID provider can accept empty string to represent default scope.
322322
:param str redirect_uri:
323323
Optional. If not specified, server will use the pre-registered one.
324324
:param str state:
@@ -998,6 +998,78 @@ def __init__(self, client_id, client_credential=None, **kwargs):
998998
super(PublicClientApplication, self).__init__(
999999
client_id, client_credential=None, **kwargs)
10001000

1001+
def acquire_token_interactive(
1002+
self,
1003+
scopes, # type: list[str]
1004+
prompt=None,
1005+
login_hint=None, # type: Optional[str]
1006+
domain_hint=None, # type: Optional[str]
1007+
claims_challenge=None,
1008+
timeout=None,
1009+
port=None,
1010+
**kwargs):
1011+
"""Acquire token interactively i.e. via a local browser.
1012+
1013+
:param list scope:
1014+
It is a list of case-sensitive strings.
1015+
:param str prompt:
1016+
By default, no prompt value will be sent, not even "none".
1017+
You will have to specify a value explicitly.
1018+
Its valid values are defined in Open ID Connect specs
1019+
https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
1020+
:param str login_hint:
1021+
Optional. Identifier of the user. Generally a User Principal Name (UPN).
1022+
:param domain_hint:
1023+
Can be one of "consumers" or "organizations" or your tenant domain "contoso.com".
1024+
If included, it will skip the email-based discovery process that user goes
1025+
through on the sign-in page, leading to a slightly more streamlined user experience.
1026+
More information on possible values
1027+
`here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
1028+
`here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
1029+
1030+
:param claims_challenge:
1031+
The claims_challenge parameter requests specific claims requested by the resource provider
1032+
in the form of a claims_challenge directive in the www-authenticate header to be
1033+
returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
1034+
It is a string of a JSON object which contains lists of claims being requested from these locations.
1035+
1036+
:param int timeout:
1037+
This method will block the current thread.
1038+
This parameter specifies the timeout value in seconds.
1039+
Default value ``None`` means wait indefinitely.
1040+
1041+
:param int port:
1042+
The port to be used to listen to an incoming auth response.
1043+
By default we will use a system-allocated port.
1044+
(The rest of the redirect_uri is hard coded as ``http://localhost``.)
1045+
1046+
:return:
1047+
- A dict containing no "error" key,
1048+
and typically contains an "access_token" key,
1049+
if cache lookup succeeded.
1050+
- A dict containing an "error" key, when token refresh failed.
1051+
"""
1052+
self._validate_ssh_cert_input_data(kwargs.get("data", {}))
1053+
claims = _merge_claims_challenge_and_capabilities(
1054+
self._client_capabilities, claims_challenge)
1055+
return self.client.obtain_token_by_browser(
1056+
scope=decorate_scope(scopes, self.client_id) if scopes else None,
1057+
redirect_uri="http://localhost:{port}".format(
1058+
# Hardcode the host, for now. AAD portal rejects 127.0.0.1 anyway
1059+
port=port or 0),
1060+
prompt=prompt,
1061+
login_hint=login_hint,
1062+
domain_hint=domain_hint,
1063+
timeout=timeout,
1064+
auth_params={"claims": claims},
1065+
data=dict(kwargs.pop("data", {}), claims=claims),
1066+
headers={
1067+
CLIENT_REQUEST_ID: _get_new_correlation_id(),
1068+
CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header(
1069+
self.ACQUIRE_TOKEN_INTERACTIVE),
1070+
},
1071+
**kwargs)
1072+
10011073
def initiate_device_flow(self, scopes=None, **kwargs):
10021074
"""Initiate a Device Flow instance,
10031075
which will be used in :func:`~acquire_token_by_device_flow`.

tests/test_e2e.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,39 @@ def _test_acquire_token_by_auth_code_flow(
441441
error_description=result.get("error_description")))
442442
self.assertCacheWorksForUser(result, scope, username=None)
443443

444+
def _test_acquire_token_interactive(
445+
self, client_id=None, authority=None, scope=None, port=None,
446+
username_uri="", # But you would want to provide one
447+
**ignored):
448+
assert client_id and authority and scope
449+
self.app = msal.PublicClientApplication(
450+
client_id, authority=authority, http_client=MinimalHttpClient())
451+
result = self.app.acquire_token_interactive(
452+
scope,
453+
timeout=60,
454+
port=port,
455+
welcome_template= # This is an undocumented feature for testing
456+
"""<html><body><h1>{id}</h1><ol>
457+
<li>Get a username from the upn shown at <a href="{username_uri}">here</a></li>
458+
<li>Get its password from https://aka.ms/GetLabUserSecret?Secret=msidlabXYZ
459+
(replace the lab name with the labName from the link above).</li>
460+
<li><a href="$auth_uri">Sign In</a> or <a href="$abort_uri">Abort</a></li>
461+
</ol></body></html>""".format(id=self.id(), username_uri=username_uri),
462+
)
463+
logger.debug(
464+
"%s: cache = %s, id_token_claims = %s",
465+
self.id(),
466+
json.dumps(self.app.token_cache._cache, indent=4),
467+
json.dumps(result.get("id_token_claims"), indent=4),
468+
)
469+
self.assertIn(
470+
"access_token", result,
471+
"{error}: {error_description}".format(
472+
# Note: No interpolation here, cause error won't always present
473+
error=result.get("error"),
474+
error_description=result.get("error_description")))
475+
self.assertCacheWorksForUser(result, scope, username=None)
476+
444477
def _test_acquire_token_obo(self, config_pca, config_cca):
445478
# 1. An app obtains a token representing a user, for our mid-tier service
446479
pca = msal.PublicClientApplication(
@@ -525,6 +558,13 @@ def test_adfs2019_fed_user(self):
525558
self.skipTest("MEX endpoint in our test environment tends to fail")
526559
raise
527560

561+
@unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented")
562+
def test_cloud_acquire_token_interactive(self):
563+
config = self.get_lab_user(usertype="cloud")
564+
self._test_acquire_token_interactive(
565+
username_uri="https://msidlab.com/api/user?usertype=cloud",
566+
**config)
567+
528568
def test_ropc_adfs2019_onprem(self):
529569
# Configuration is derived from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.7.0/tests/Microsoft.Identity.Test.Common/TestConstants.cs#L250-L259
530570
config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019")
@@ -557,6 +597,16 @@ def test_adfs2019_onprem_acquire_token_by_auth_code_flow(self):
557597
username_uri="https://msidlab.com/api/user?usertype=onprem&federationprovider=ADFSv2019",
558598
**config)
559599

600+
@unittest.skipIf(os.getenv("TRAVIS"), "Browser automation is not yet implemented")
601+
def test_adfs2019_onprem_acquire_token_interactive(self):
602+
config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019")
603+
config["authority"] = "https://fs.%s.com/adfs" % config["lab_name"]
604+
config["scope"] = self.adfs2019_scopes
605+
config["port"] = 8080
606+
self._test_acquire_token_interactive(
607+
username_uri="https://msidlab.com/api/user?usertype=onprem&federationprovider=ADFSv2019",
608+
**config)
609+
560610
@unittest.skipUnless(
561611
os.getenv("LAB_OBO_CLIENT_SECRET"),
562612
"Need LAB_OBO_CLIENT SECRET from https://msidlabs.vault.azure.net/secrets/TodoListServiceV2-OBO/c58ba97c34ca4464886943a847d1db56")

0 commit comments

Comments
 (0)