@@ -300,6 +300,79 @@ def _build_client(self, client_credential, authority):
300300 on_removing_rt = self .token_cache .remove_rt ,
301301 on_updating_rt = self .token_cache .update_rt )
302302
303+ def initiate_auth_code_flow (
304+ self ,
305+ scopes , # type: list[str]
306+ redirect_uri = None ,
307+ state = None , # Recommended by OAuth2 for CSRF protection
308+ prompt = None ,
309+ login_hint = None , # type: Optional[str]
310+ domain_hint = None , # type: Optional[str]
311+ claims_challenge = None ,
312+ ):
313+ """Initiate an auth code flow.
314+
315+ Later when the response reaches your redirect_uri,
316+ you can use :func:`~acquire_token_by_auth_code_flow()`
317+ to complete the authentication/authorization.
318+
319+ :param list scope:
320+ It is a list of case-sensitive strings.
321+ Some ID provider can accept empty string to represent default scope.
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+
303376 def get_authorization_request_url (
304377 self ,
305378 scopes , # type: list[str]
@@ -386,6 +459,73 @@ def get_authorization_request_url(
386459 self ._client_capabilities , claims_challenge ),
387460 )
388461
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+
389529 def acquire_token_by_authorization_code (
390530 self ,
391531 code ,
0 commit comments