|
21 | 21 |
|
22 | 22 |
|
23 | 23 | # The __init__.py will import this. Not the other way around.
|
24 |
| -__version__ = "1.6.0" |
| 24 | +__version__ = "1.7.0" |
25 | 25 |
|
26 | 26 | logger = logging.getLogger(__name__)
|
27 | 27 |
|
@@ -107,6 +107,7 @@ class ClientApplication(object):
|
107 | 107 | ACQUIRE_TOKEN_BY_DEVICE_FLOW_ID = "622"
|
108 | 108 | ACQUIRE_TOKEN_FOR_CLIENT_ID = "730"
|
109 | 109 | ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID = "832"
|
| 110 | + ACQUIRE_TOKEN_INTERACTIVE = "169" |
110 | 111 | GET_ACCOUNTS_ID = "902"
|
111 | 112 | REMOVE_ACCOUNT_ID = "903"
|
112 | 113 |
|
@@ -300,6 +301,78 @@ def _build_client(self, client_credential, authority):
|
300 | 301 | on_removing_rt=self.token_cache.remove_rt,
|
301 | 302 | on_updating_rt=self.token_cache.update_rt)
|
302 | 303 |
|
| 304 | + def initiate_auth_code_flow( |
| 305 | + self, |
| 306 | + scopes, # type: list[str] |
| 307 | + redirect_uri=None, |
| 308 | + state=None, # Recommended by OAuth2 for CSRF protection |
| 309 | + prompt=None, |
| 310 | + login_hint=None, # type: Optional[str] |
| 311 | + domain_hint=None, # type: Optional[str] |
| 312 | + claims_challenge=None, |
| 313 | + ): |
| 314 | + """Initiate an auth code flow. |
| 315 | +
|
| 316 | + Later when the response reaches your redirect_uri, |
| 317 | + you can use :func:`~acquire_token_by_auth_code_flow()` |
| 318 | + to complete the authentication/authorization. |
| 319 | +
|
| 320 | + :param list scope: |
| 321 | + It is a list of case-sensitive strings. |
| 322 | + :param str redirect_uri: |
| 323 | + Optional. If not specified, server will use the pre-registered one. |
| 324 | + :param str state: |
| 325 | + An opaque value used by the client to |
| 326 | + maintain state between the request and callback. |
| 327 | + If absent, this library will automatically generate one internally. |
| 328 | + :param str prompt: |
| 329 | + By default, no prompt value will be sent, not even "none". |
| 330 | + You will have to specify a value explicitly. |
| 331 | + Its valid values are defined in Open ID Connect specs |
| 332 | + https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest |
| 333 | + :param str login_hint: |
| 334 | + Optional. Identifier of the user. Generally a User Principal Name (UPN). |
| 335 | + :param domain_hint: |
| 336 | + Can be one of "consumers" or "organizations" or your tenant domain "contoso.com". |
| 337 | + If included, it will skip the email-based discovery process that user goes |
| 338 | + through on the sign-in page, leading to a slightly more streamlined user experience. |
| 339 | + More information on possible values |
| 340 | + `here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and |
| 341 | + `here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_. |
| 342 | +
|
| 343 | + :return: |
| 344 | + The auth code flow. It is a dict in this form:: |
| 345 | +
|
| 346 | + { |
| 347 | + "auth_uri": "https://...", // Guide user to visit this |
| 348 | + "state": "...", // You may choose to verify it by yourself, |
| 349 | + // or just let acquire_token_by_auth_code_flow() |
| 350 | + // do that for you. |
| 351 | + "...": "...", // Everything else are reserved and internal |
| 352 | + } |
| 353 | +
|
| 354 | + The caller is expected to:: |
| 355 | +
|
| 356 | + 1. somehow store this content, typically inside the current session, |
| 357 | + 2. guide the end user (i.e. resource owner) to visit that auth_uri, |
| 358 | + 3. and then relay this dict and subsequent auth response to |
| 359 | + :func:`~acquire_token_by_auth_code_flow()`. |
| 360 | + """ |
| 361 | + client = Client( |
| 362 | + {"authorization_endpoint": self.authority.authorization_endpoint}, |
| 363 | + self.client_id, |
| 364 | + http_client=self.http_client) |
| 365 | + flow = client.initiate_auth_code_flow( |
| 366 | + redirect_uri=redirect_uri, state=state, login_hint=login_hint, |
| 367 | + prompt=prompt, |
| 368 | + scope=decorate_scope(scopes, self.client_id), |
| 369 | + domain_hint=domain_hint, |
| 370 | + claims=_merge_claims_challenge_and_capabilities( |
| 371 | + self._client_capabilities, claims_challenge), |
| 372 | + ) |
| 373 | + flow["claims_challenge"] = claims_challenge |
| 374 | + return flow |
| 375 | + |
303 | 376 | def get_authorization_request_url(
|
304 | 377 | self,
|
305 | 378 | scopes, # type: list[str]
|
@@ -386,6 +459,73 @@ def get_authorization_request_url(
|
386 | 459 | self._client_capabilities, claims_challenge),
|
387 | 460 | )
|
388 | 461 |
|
| 462 | + def acquire_token_by_auth_code_flow( |
| 463 | + self, auth_code_flow, auth_response, scopes=None, **kwargs): |
| 464 | + """Validate the auth response being redirected back, and obtain tokens. |
| 465 | +
|
| 466 | + It automatically provides nonce protection. |
| 467 | +
|
| 468 | + :param dict auth_code_flow: |
| 469 | + The same dict returned by :func:`~initiate_auth_code_flow()`. |
| 470 | + :param dict auth_response: |
| 471 | + A dict of the query string received from auth server. |
| 472 | + :param list[str] scopes: |
| 473 | + Scopes requested to access a protected API (a resource). |
| 474 | +
|
| 475 | + Most of the time, you can leave it empty. |
| 476 | +
|
| 477 | + If you requested user consent for multiple resources, here you will |
| 478 | + need to provide a subset of what you required in |
| 479 | + :func:`~initiate_auth_code_flow()`. |
| 480 | +
|
| 481 | + OAuth2 was designed mostly for singleton services, |
| 482 | + where tokens are always meant for the same resource and the only |
| 483 | + changes are in the scopes. |
| 484 | + In AAD, tokens can be issued for multiple 3rd party resources. |
| 485 | + You can ask authorization code for multiple resources, |
| 486 | + but when you redeem it, the token is for only one intended |
| 487 | + recipient, called audience. |
| 488 | + So the developer need to specify a scope so that we can restrict the |
| 489 | + token to be issued for the corresponding audience. |
| 490 | +
|
| 491 | + :return: |
| 492 | + * A dict containing "access_token" and/or "id_token", among others, |
| 493 | + depends on what scope was used. |
| 494 | + (See https://tools.ietf.org/html/rfc6749#section-5.1) |
| 495 | + * A dict containing "error", optionally "error_description", "error_uri". |
| 496 | + (It is either `this <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>`_ |
| 497 | + or `that <https://tools.ietf.org/html/rfc6749#section-5.2>`_) |
| 498 | + * Most client-side data error would result in ValueError exception. |
| 499 | + So the usage pattern could be without any protocol details:: |
| 500 | +
|
| 501 | + def authorize(): # A controller in a web app |
| 502 | + try: |
| 503 | + result = msal_app.acquire_token_by_auth_code_flow( |
| 504 | + session.get("flow", {}), request.args) |
| 505 | + if "error" in result: |
| 506 | + return render_template("error.html", result) |
| 507 | + use(result) # Token(s) are available in result and cache |
| 508 | + except ValueError: # Usually caused by CSRF |
| 509 | + pass # Simply ignore them |
| 510 | + return redirect(url_for("index")) |
| 511 | + """ |
| 512 | + self._validate_ssh_cert_input_data(kwargs.get("data", {})) |
| 513 | + return self.client.obtain_token_by_auth_code_flow( |
| 514 | + auth_code_flow, |
| 515 | + auth_response, |
| 516 | + scope=decorate_scope(scopes, self.client_id) if scopes else None, |
| 517 | + headers={ |
| 518 | + CLIENT_REQUEST_ID: _get_new_correlation_id(), |
| 519 | + CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( |
| 520 | + self.ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID), |
| 521 | + }, |
| 522 | + data=dict( |
| 523 | + kwargs.pop("data", {}), |
| 524 | + claims=_merge_claims_challenge_and_capabilities( |
| 525 | + self._client_capabilities, |
| 526 | + auth_code_flow.pop("claims_challenge", None))), |
| 527 | + **kwargs) |
| 528 | + |
389 | 529 | def acquire_token_by_authorization_code(
|
390 | 530 | self,
|
391 | 531 | code,
|
@@ -858,6 +998,80 @@ def __init__(self, client_id, client_credential=None, **kwargs):
|
858 | 998 | super(PublicClientApplication, self).__init__(
|
859 | 999 | client_id, client_credential=None, **kwargs)
|
860 | 1000 |
|
| 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 | + timeout=timeout, |
| 1063 | + auth_params={ |
| 1064 | + "claims": claims, |
| 1065 | + "domain_hint": domain_hint, |
| 1066 | + }, |
| 1067 | + data=dict(kwargs.pop("data", {}), claims=claims), |
| 1068 | + headers={ |
| 1069 | + CLIENT_REQUEST_ID: _get_new_correlation_id(), |
| 1070 | + CLIENT_CURRENT_TELEMETRY: _build_current_telemetry_request_header( |
| 1071 | + self.ACQUIRE_TOKEN_INTERACTIVE), |
| 1072 | + }, |
| 1073 | + **kwargs) |
| 1074 | + |
861 | 1075 | def initiate_device_flow(self, scopes=None, **kwargs):
|
862 | 1076 | """Initiate a Device Flow instance,
|
863 | 1077 | which will be used in :func:`~acquire_token_by_device_flow`.
|
|
0 commit comments