@@ -79,6 +79,17 @@ def extract_certs(public_cert_content):
7979 return [public_cert_content .strip ()]
8080
8181
82+ def _merge_claims_challenge_and_capabilities (capabilities , claims_challenge ):
83+ # Represent capabilities as {"access_token": {"xms_cc": {"values": capabilities}}}
84+ # and then merge/add it into incoming claims
85+ if not capabilities :
86+ return claims_challenge
87+ claims_dict = json .loads (claims_challenge ) if claims_challenge else {}
88+ for key in ["access_token" ]: # We could add "id_token" if we'd decide to
89+ claims_dict .setdefault (key , {}).update (xms_cc = {"values" : capabilities })
90+ return json .dumps (claims_dict )
91+
92+
8293class ClientApplication (object ):
8394
8495 ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -97,7 +108,8 @@ def __init__(
97108 token_cache = None ,
98109 http_client = None ,
99110 verify = True , proxies = None , timeout = None ,
100- client_claims = None , app_name = None , app_version = None ):
111+ client_claims = None , app_name = None , app_version = None ,
112+ client_capabilities = None ):
101113 """Create an instance of application.
102114
103115 :param str client_id: Your app has a client_id after you register it on AAD.
@@ -179,10 +191,16 @@ def __init__(
179191 :param app_version: (optional)
180192 You can provide your application version for Microsoft telemetry purposes.
181193 Default value is None, means it will not be passed to Microsoft.
194+ :param list[str] client_capabilities: (optional)
195+ Allows configuration of one or more client capabilities, e.g. ["CP1"].
196+ MSAL will combine them into
197+ `claims parameter <https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter`_
198+ which you will later provide via one of the acquire-token request.
182199 """
183200 self .client_id = client_id
184201 self .client_credential = client_credential
185202 self .client_claims = client_claims
203+ self ._client_capabilities = client_capabilities
186204 if http_client :
187205 self .http_client = http_client
188206 else :
@@ -261,6 +279,7 @@ def get_authorization_request_url(
261279 prompt = None ,
262280 nonce = None ,
263281 domain_hint = None , # type: Optional[str]
282+ claims_challenge = None ,
264283 ** kwargs ):
265284 """Constructs a URL for you to start a Authorization Code Grant.
266285
@@ -289,6 +308,12 @@ def get_authorization_request_url(
289308 More information on possible values
290309 `here <https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code>`_ and
291310 `here <https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oapx/86fb452d-e34a-494e-ac61-e526e263b6d8>`_.
311+ :param claims_challenge:
312+ The claims_challenge parameter requests specific claims requested by the resource provider
313+ in the form of a claims_challenge directive in the www-authenticate header to be
314+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
315+ It is a string of a JSON object which contains lists of claims being requested from these locations.
316+
292317 :return: The authorization url as a string.
293318 """
294319 """ # TBD: this would only be meaningful in a new acquire_token_interactive()
@@ -321,6 +346,8 @@ def get_authorization_request_url(
321346 scope = decorate_scope (scopes , self .client_id ),
322347 nonce = nonce ,
323348 domain_hint = domain_hint ,
349+ claims = _merge_claims_challenge_and_capabilities (
350+ self ._client_capabilities , claims_challenge ),
324351 )
325352
326353 def acquire_token_by_authorization_code (
@@ -332,6 +359,7 @@ def acquire_token_by_authorization_code(
332359 # authorization request as described in Section 4.1.1, and their
333360 # values MUST be identical.
334361 nonce = None ,
362+ claims_challenge = None ,
335363 ** kwargs ):
336364 """The second half of the Authorization Code Grant.
337365
@@ -357,6 +385,12 @@ def acquire_token_by_authorization_code(
357385 same nonce should also be provided here, so that we'll validate it.
358386 An exception will be raised if the nonce in id token mismatches.
359387
388+ :param claims_challenge:
389+ The claims_challenge parameter requests specific claims requested by the resource provider
390+ in the form of a claims_challenge directive in the www-authenticate header to be
391+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
392+ It is a string of a JSON object which contains lists of claims being requested from these locations.
393+
360394 :return: A dict representing the json response from AAD:
361395
362396 - A successful response would contain "access_token" key,
@@ -377,6 +411,10 @@ def acquire_token_by_authorization_code(
377411 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
378412 self .ACQUIRE_TOKEN_BY_AUTHORIZATION_CODE_ID ),
379413 },
414+ data = dict (
415+ kwargs .pop ("data" , {}),
416+ claims = _merge_claims_challenge_and_capabilities (
417+ self ._client_capabilities , claims_challenge )),
380418 nonce = nonce ,
381419 ** kwargs )
382420
@@ -479,6 +517,7 @@ def acquire_token_silent(
479517 account , # type: Optional[Account]
480518 authority = None , # See get_authorization_request_url()
481519 force_refresh = False , # type: Optional[boolean]
520+ claims_challenge = None ,
482521 ** kwargs ):
483522 """Acquire an access token for given account, without user interaction.
484523
@@ -493,14 +532,21 @@ def acquire_token_silent(
493532
494533 Internally, this method calls :func:`~acquire_token_silent_with_error`.
495534
535+ :param claims_challenge:
536+ The claims_challenge parameter requests specific claims requested by the resource provider
537+ in the form of a claims_challenge directive in the www-authenticate header to be
538+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
539+ It is a string of a JSON object which contains lists of claims being requested from these locations.
540+
496541 :return:
497542 - A dict containing no "error" key,
498543 and typically contains an "access_token" key,
499544 if cache lookup succeeded.
500545 - None when cache lookup does not yield a token.
501546 """
502547 result = self .acquire_token_silent_with_error (
503- scopes , account , authority , force_refresh , ** kwargs )
548+ scopes , account , authority , force_refresh ,
549+ claims_challenge = claims_challenge , ** kwargs )
504550 return result if result and "error" not in result else None
505551
506552 def acquire_token_silent_with_error (
@@ -509,6 +555,7 @@ def acquire_token_silent_with_error(
509555 account , # type: Optional[Account]
510556 authority = None , # See get_authorization_request_url()
511557 force_refresh = False , # type: Optional[boolean]
558+ claims_challenge = None ,
512559 ** kwargs ):
513560 """Acquire an access token for given account, without user interaction.
514561
@@ -529,6 +576,11 @@ def acquire_token_silent_with_error(
529576 :param force_refresh:
530577 If True, it will skip Access Token look-up,
531578 and try to find a Refresh Token to obtain a new Access Token.
579+ :param claims_challenge:
580+ The claims_challenge parameter requests specific claims requested by the resource provider
581+ in the form of a claims_challenge directive in the www-authenticate header to be
582+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
583+ It is a string of a JSON object which contains lists of claims being requested from these locations.
532584 :return:
533585 - A dict containing no "error" key,
534586 and typically contains an "access_token" key,
@@ -547,6 +599,7 @@ def acquire_token_silent_with_error(
547599 # ) if authority else self.authority
548600 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
549601 scopes , account , self .authority , force_refresh = force_refresh ,
602+ claims_challenge = claims_challenge ,
550603 correlation_id = correlation_id ,
551604 ** kwargs )
552605 if result and "error" not in result :
@@ -567,6 +620,7 @@ def acquire_token_silent_with_error(
567620 validate_authority = False )
568621 result = self ._acquire_token_silent_from_cache_and_possibly_refresh_it (
569622 scopes , account , the_authority , force_refresh = force_refresh ,
623+ claims_challenge = claims_challenge ,
570624 correlation_id = correlation_id ,
571625 ** kwargs )
572626 if result :
@@ -589,8 +643,9 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
589643 account , # type: Optional[Account]
590644 authority , # This can be different than self.authority
591645 force_refresh = False , # type: Optional[boolean]
646+ claims_challenge = None ,
592647 ** kwargs ):
593- if not force_refresh :
648+ if not ( force_refresh or claims_challenge ): # Bypass AT when desired or using claims
594649 query = {
595650 "client_id" : self .client_id ,
596651 "environment" : authority .instance ,
@@ -617,7 +672,7 @@ def _acquire_token_silent_from_cache_and_possibly_refresh_it(
617672 }
618673 return self ._acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
619674 authority , decorate_scope (scopes , self .client_id ), account ,
620- force_refresh = force_refresh , ** kwargs )
675+ force_refresh = force_refresh , claims_challenge = claims_challenge , ** kwargs )
621676
622677 def _acquire_token_silent_by_finding_rt_belongs_to_me_or_my_family (
623678 self , authority , scopes , account , ** kwargs ):
@@ -666,7 +721,7 @@ def _get_app_metadata(self, environment):
666721 def _acquire_token_silent_by_finding_specific_refresh_token (
667722 self , authority , scopes , query ,
668723 rt_remover = None , break_condition = lambda response : False ,
669- force_refresh = False , correlation_id = None , ** kwargs ):
724+ force_refresh = False , correlation_id = None , claims_challenge = None , ** kwargs ):
670725 matches = self .token_cache .find (
671726 self .token_cache .CredentialType .REFRESH_TOKEN ,
672727 # target=scopes, # AAD RTs are scope-independent
@@ -686,6 +741,10 @@ def _acquire_token_silent_by_finding_specific_refresh_token(
686741 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
687742 self .ACQUIRE_TOKEN_SILENT_ID , force_refresh = force_refresh ),
688743 },
744+ data = dict (
745+ kwargs .pop ("data" , {}),
746+ claims = _merge_claims_challenge_and_capabilities (
747+ self ._client_capabilities , claims_challenge )),
689748 ** kwargs )
690749 if "error" not in response :
691750 return response
@@ -780,14 +839,19 @@ def initiate_device_flow(self, scopes=None, **kwargs):
780839 flow [self .DEVICE_FLOW_CORRELATION_ID ] = correlation_id
781840 return flow
782841
783- def acquire_token_by_device_flow (self , flow , ** kwargs ):
842+ def acquire_token_by_device_flow (self , flow , claims_challenge = None , ** kwargs ):
784843 """Obtain token by a device flow object, with customizable polling effect.
785844
786845 :param dict flow:
787846 A dict previously generated by :func:`~initiate_device_flow`.
788847 By default, this method's polling effect will block current thread.
789848 You can abort the polling loop at any time,
790849 by changing the value of the flow's "expires_at" key to 0.
850+ :param claims_challenge:
851+ The claims_challenge parameter requests specific claims requested by the resource provider
852+ in the form of a claims_challenge directive in the www-authenticate header to be
853+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
854+ It is a string of a JSON object which contains lists of claims being requested from these locations.
791855
792856 :return: A dict representing the json response from AAD:
793857
@@ -796,10 +860,14 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
796860 """
797861 return self .client .obtain_token_by_device_flow (
798862 flow ,
799- data = dict (kwargs .pop ("data" , {}), code = flow ["device_code" ]),
800- # 2018-10-4 Hack:
801- # during transition period,
802- # service seemingly need both device_code and code parameter.
863+ data = dict (
864+ kwargs .pop ("data" , {}),
865+ code = flow ["device_code" ], # 2018-10-4 Hack:
866+ # during transition period,
867+ # service seemingly need both device_code and code parameter.
868+ claims = _merge_claims_challenge_and_capabilities (
869+ self ._client_capabilities , claims_challenge ),
870+ ),
803871 headers = {
804872 CLIENT_REQUEST_ID :
805873 flow .get (self .DEVICE_FLOW_CORRELATION_ID ) or _get_new_correlation_id (),
@@ -809,7 +877,7 @@ def acquire_token_by_device_flow(self, flow, **kwargs):
809877 ** kwargs )
810878
811879 def acquire_token_by_username_password (
812- self , username , password , scopes , ** kwargs ):
880+ self , username , password , scopes , claims_challenge = None , ** kwargs ):
813881 """Gets a token for a given resource via user credentials.
814882
815883 See this page for constraints of Username Password Flow.
@@ -819,6 +887,11 @@ def acquire_token_by_username_password(
819887 :param str password: The password.
820888 :param list[str] scopes:
821889 Scopes requested to access a protected API (a resource).
890+ :param claims_challenge:
891+ The claims_challenge parameter requests specific claims requested by the resource provider
892+ in the form of a claims_challenge directive in the www-authenticate header to be
893+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
894+ It is a string of a JSON object which contains lists of claims being requested from these locations.
822895
823896 :return: A dict representing the json response from AAD:
824897
@@ -831,16 +904,22 @@ def acquire_token_by_username_password(
831904 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
832905 self .ACQUIRE_TOKEN_BY_USERNAME_PASSWORD_ID ),
833906 }
907+ data = dict (
908+ kwargs .pop ("data" , {}),
909+ claims = _merge_claims_challenge_and_capabilities (
910+ self ._client_capabilities , claims_challenge ))
834911 if not self .authority .is_adfs :
835912 user_realm_result = self .authority .user_realm_discovery (
836913 username , correlation_id = headers [CLIENT_REQUEST_ID ])
837914 if user_realm_result .get ("account_type" ) == "Federated" :
838915 return self ._acquire_token_by_username_password_federated (
839916 user_realm_result , username , password , scopes = scopes ,
917+ data = data ,
840918 headers = headers , ** kwargs )
841919 return self .client .obtain_token_by_username_password (
842920 username , password , scope = scopes ,
843921 headers = headers ,
922+ data = data ,
844923 ** kwargs )
845924
846925 def _acquire_token_by_username_password_federated (
@@ -882,11 +961,16 @@ def _acquire_token_by_username_password_federated(
882961
883962class ConfidentialClientApplication (ClientApplication ): # server-side web app
884963
885- def acquire_token_for_client (self , scopes , ** kwargs ):
964+ def acquire_token_for_client (self , scopes , claims_challenge = None , ** kwargs ):
886965 """Acquires token for the current confidential client, not for an end user.
887966
888967 :param list[str] scopes: (Required)
889968 Scopes requested to access a protected API (a resource).
969+ :param claims_challenge:
970+ The claims_challenge parameter requests specific claims requested by the resource provider
971+ in the form of a claims_challenge directive in the www-authenticate header to be
972+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
973+ It is a string of a JSON object which contains lists of claims being requested from these locations.
890974
891975 :return: A dict representing the json response from AAD:
892976
@@ -901,9 +985,13 @@ def acquire_token_for_client(self, scopes, **kwargs):
901985 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
902986 self .ACQUIRE_TOKEN_FOR_CLIENT_ID ),
903987 },
988+ data = dict (
989+ kwargs .pop ("data" , {}),
990+ claims = _merge_claims_challenge_and_capabilities (
991+ self ._client_capabilities , claims_challenge )),
904992 ** kwargs )
905993
906- def acquire_token_on_behalf_of (self , user_assertion , scopes , ** kwargs ):
994+ def acquire_token_on_behalf_of (self , user_assertion , scopes , claims_challenge = None , ** kwargs ):
907995 """Acquires token using on-behalf-of (OBO) flow.
908996
909997 The current app is a middle-tier service which was called with a token
@@ -918,6 +1006,11 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
9181006
9191007 :param str user_assertion: The incoming token already received by this app
9201008 :param list[str] scopes: Scopes required by downstream API (a resource).
1009+ :param claims_challenge:
1010+ The claims_challenge parameter requests specific claims requested by the resource provider
1011+ in the form of a claims_challenge directive in the www-authenticate header to be
1012+ returned from the UserInfo Endpoint and/or in the ID Token and/or Access Token.
1013+ It is a string of a JSON object which contains lists of claims being requested from these locations..
9211014
9221015 :return: A dict representing the json response from AAD:
9231016
@@ -935,7 +1028,11 @@ def acquire_token_on_behalf_of(self, user_assertion, scopes, **kwargs):
9351028 # 2. Requesting an IDT (which would otherwise be unavailable)
9361029 # so that the calling app could use id_token_claims to implement
9371030 # their own cache mapping, which is likely needed in web apps.
938- data = dict (kwargs .pop ("data" , {}), requested_token_use = "on_behalf_of" ),
1031+ data = dict (
1032+ kwargs .pop ("data" , {}),
1033+ requested_token_use = "on_behalf_of" ,
1034+ claims = _merge_claims_challenge_and_capabilities (
1035+ self ._client_capabilities , claims_challenge )),
9391036 headers = {
9401037 CLIENT_REQUEST_ID : _get_new_correlation_id (),
9411038 CLIENT_CURRENT_TELEMETRY : _build_current_telemetry_request_header (
0 commit comments