@@ -300,6 +300,79 @@ def _build_client(self, client_credential, authority):
300
300
on_removing_rt = self .token_cache .remove_rt ,
301
301
on_updating_rt = self .token_cache .update_rt )
302
302
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
+
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 ,
0 commit comments